mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[2 of 3] Support long pasted text in TUI goals (#27509)
## Stack 1. [1 of 3] Support long raw TUI goal objectives - #27508 2. **[2 of 3] Support long pasted text in TUI goals** - this PR 3. [3 of 3] Support images in TUI goals - #27510 ## Why Large text pasted into the TUI composer is represented as a paste placeholder plus pending paste metadata. For `/goal`, preserving only the visible placeholder is not enough: the agent would see a short placeholder string instead of the actual pasted text, and the long-text support from the first PR would never see the payload. The TUI also needs to avoid writing stale sidecar files when a user pastes a large block and then deletes its placeholder before submitting the goal. ## What Changed - Introduces a TUI `GoalDraft` for goal submissions so `/goal`, `/goal edit`, and queued goal commands can carry objective text plus text elements and pending paste payloads. - Materializes active pasted-text placeholders to `pasted-text-N.txt` files through the app-server filesystem path introduced in #27508. - Rewrites active paste placeholders in the persisted objective to file references, while leaving literal placeholder-looking text alone. - Filters out deleted paste placeholders so otherwise-small goals do not require `$CODEX_HOME` or remote filesystem writes. - Preserves pending paste metadata when a `/goal` command is queued before a thread exists. ## Verification - Added goal materialization tests for active paste placeholders, deleted paste placeholders, and whitespace-only paste payloads. - Added/updated TUI slash-command tests for large pasted text, queued `/goal` commands before thread start, and queued oversized goal behavior. ## Manual Testing - Used real terminal bracketed-paste sequences through a remote TUI session. A 1,228-byte multiline paste became `pasted-text-1.txt`; its first/last lines and byte count matched exactly, and the persisted objective referenced the server-host path. - Pasted a large block, deleted its placeholder, and submitted a small replacement objective. No new directory or sidecar file was created. - Added two same-length large pastes to one goal. The composer disambiguated their visible placeholders, and materialization preserved order and contents in `pasted-text-1.txt` and `pasted-text-2.txt`. - Submitted a whitespace-only large paste and verified the goal was rejected as empty without writing a file. - Submitted a pasted-text replacement while another goal was active, verified no file was written before confirmation, then canceled and confirmed the original goal remained unchanged. - Combined a large paste with enough raw text to exceed 4,000 characters after placeholder rewriting. The paste sidecar and `goal-objective.md` were created in the same remote attachment directory, and `/goal edit` restored the rewritten objective with its sidecar reference.
This commit is contained in:
1 parent
216dee1189
commit
c0f1ec5afd
17 files changed
+600
-201
No files matched your search
@@ -13,6 +13,7 @@ use crate::bottom_pane::slash_commands::ServiceTierCommand;
|
||||
use crate::bottom_pane::slash_commands::SlashCommandItem;
|
||||
use crate::bottom_pane::slash_commands::find_slash_command;
|
||||
use crate::goal_display::GOAL_USAGE;
|
||||
use crate::goal_files::GoalDraft;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum SlashCommandDispatchSource {
|
||||
@@ -23,6 +24,7 @@ enum SlashCommandDispatchSource {
|
||||
struct PreparedSlashCommandArgs {
|
||||
args: String,
|
||||
text_elements: Vec<TextElement>,
|
||||
pending_pastes: Vec<(String, String)>,
|
||||
local_images: Vec<LocalImageAttachment>,
|
||||
remote_image_urls: Vec<String>,
|
||||
mention_bindings: Vec<MentionBinding>,
|
||||
@@ -553,6 +555,22 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
if cmd == SlashCommand::Goal {
|
||||
self.dispatch_prepared_command_with_args(
|
||||
cmd,
|
||||
PreparedSlashCommandArgs {
|
||||
args,
|
||||
text_elements,
|
||||
pending_pastes: self.bottom_pane.composer_pending_pastes(),
|
||||
local_images: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
mention_bindings: Vec::new(),
|
||||
source: SlashCommandDispatchSource::Live,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some((prepared_args, prepared_elements)) =
|
||||
self.prepare_live_inline_args(args, text_elements)
|
||||
else {
|
||||
@@ -563,6 +581,7 @@ impl ChatWidget {
|
||||
PreparedSlashCommandArgs {
|
||||
args: prepared_args,
|
||||
text_elements: prepared_elements,
|
||||
pending_pastes: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
mention_bindings: Vec::new(),
|
||||
@@ -587,6 +606,7 @@ impl ChatWidget {
|
||||
fn clear_live_goal_submission(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_composer_text(String::new(), Vec::new(), Vec::new());
|
||||
self.bottom_pane.set_composer_pending_pastes(Vec::new());
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
|
||||
@@ -623,6 +643,7 @@ impl ChatWidget {
|
||||
let PreparedSlashCommandArgs {
|
||||
args,
|
||||
text_elements,
|
||||
pending_pastes,
|
||||
local_images,
|
||||
remote_image_urls,
|
||||
mention_bindings,
|
||||
@@ -750,18 +771,34 @@ impl ChatWidget {
|
||||
}
|
||||
return;
|
||||
}
|
||||
let objective = args.trim();
|
||||
let draft = GoalDraft {
|
||||
objective: args,
|
||||
text_elements,
|
||||
pending_pastes,
|
||||
};
|
||||
let Some(thread_id) = self.thread_id else {
|
||||
if source == SlashCommandDispatchSource::Live {
|
||||
const GOAL_PREFIX: &str = "/goal ";
|
||||
let text_elements = draft
|
||||
.text_elements
|
||||
.into_iter()
|
||||
.map(|element| {
|
||||
element.map_range(|range| ByteRange {
|
||||
start: range.start + GOAL_PREFIX.len(),
|
||||
end: range.end + GOAL_PREFIX.len(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.queue_user_message_with_options(
|
||||
UserMessage {
|
||||
text: format!("/goal {args}"),
|
||||
text: format!("{GOAL_PREFIX}{}", draft.objective),
|
||||
local_images: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
text_elements,
|
||||
mention_bindings: Vec::new(),
|
||||
},
|
||||
QueuedInputAction::ParseSlash,
|
||||
draft.pending_pastes,
|
||||
);
|
||||
self.clear_live_goal_submission();
|
||||
} else {
|
||||
@@ -772,12 +809,13 @@ impl ChatWidget {
|
||||
}
|
||||
return;
|
||||
};
|
||||
self.app_event_tx.send(AppEvent::SetThreadGoalObjective {
|
||||
let history_objective = draft.objective.clone();
|
||||
self.app_event_tx.send(AppEvent::SetThreadGoalDraft {
|
||||
thread_id,
|
||||
objective: objective.to_string(),
|
||||
draft,
|
||||
mode: ThreadGoalSetMode::ConfirmIfExists,
|
||||
});
|
||||
self.append_message_history_entry(format!("/goal {trimmed}"));
|
||||
self.append_message_history_entry(format!("/goal {history_objective}"));
|
||||
if source == SlashCommandDispatchSource::Live {
|
||||
self.clear_live_goal_submission();
|
||||
}
|
||||
@@ -831,7 +869,15 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn submit_queued_slash_prompt(&mut self, user_message: UserMessage) -> QueueDrain {
|
||||
pub(super) fn submit_queued_slash_prompt(
|
||||
&mut self,
|
||||
queued_message: QueuedUserMessage,
|
||||
) -> QueueDrain {
|
||||
let QueuedUserMessage {
|
||||
user_message,
|
||||
pending_pastes,
|
||||
..
|
||||
} = queued_message;
|
||||
let UserMessage {
|
||||
text,
|
||||
local_images,
|
||||
@@ -921,6 +967,7 @@ impl ChatWidget {
|
||||
PreparedSlashCommandArgs {
|
||||
args: trimmed_rest.to_string(),
|
||||
text_elements: args_elements,
|
||||
pending_pastes,
|
||||
local_images,
|
||||
remote_image_urls,
|
||||
mention_bindings,
|
||||
|
||||
Reference in new issue
Block a user