Skip to content

Commit 08252a0

Browse files
sa-bucOpen-Codex-CLI
authored andcommitted
Attach resumed sessions in-process and add the agent view composer
Two changes that together close the gap with Claude Code's agent view UX: In-process attach. The previous Enter→resume path forced the outer CLI to tear down the TUI and re-exec into `codex resume --thread <id>`, paying the full startup cost (~2s of perceived lag). Reuse the existing `replace_chat_widget_with_app_server_thread` helper to swap chat widgets inside the running App loop instead — the new `attach_resumed_thread_from_browser` helper calls `app_server.resume_thread`, builds a `ChatWidgetInit` via the existing `chatwidget_init_for_forked_or_resumed_thread` plumbing, swaps the widget, and replays the rollout. Attach feels instantaneous now. Inline composer in the agent view. The selected row is no longer the only way to act on a session — there is a 3-line input pane at the bottom of the browser. Typing builds up a draft; Enter resumes the highlighted session with that draft as the first user message (carried through `ResumeThreadFromBrowser.initial_prompt`); `Ctrl+N` exits and re-launches a fresh session with the draft as the initial prompt; `Esc` clears a non-empty draft (and only closes the view when empty); arrow keys still navigate the list while composing, while `j`/`k`/`g`/`G`/`q` stop hijacking the alphabet once a draft is in flight. Plumbing: extend `AppExitInfo` with `resume_initial_prompt` and `start_new_session`, plus matching `AppEvent::StartNewSessionFromBrowser` for the Ctrl+N path, and propagate both through the cli/main.rs re-launch loop (the resume re-launch path stays as a fallback even though it is no longer the primary route). Smoke-verified end-to-end: Left opens the full-screen view, typing populates the composer, Enter resumes the highlighted session into the same process and immediately renders the typed message as the first user turn, codex begins working on it, no `color_spantrace::set_theme` crash. Co-authored-by: Open Codex <hff582580@gmail.com>
1 parent 7abdb39 commit 08252a0

8 files changed

Lines changed: 257 additions & 20 deletions

File tree

codex-rs/cli/src/main.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,12 +2076,24 @@ async fn run_interactive_tui(
20762076
);
20772077
let err = match attempt.await {
20782078
Ok(exit_info) => {
2079-
// If the session browser overlay asked us to resume into a
2080-
// different thread, re-launch the TUI in place so the user
2081-
// perceives an in-app "attach".
2079+
// The session browser overlay can ask the outer loop to
2080+
// relaunch. Two cases:
2081+
// * `start_new_session` — Ctrl+N: bring up a fresh codex
2082+
// session, optionally with a typed prompt.
2083+
// * `resume_thread_id` — kept as a fallback for
2084+
// non-in-process paths (most resumes are now handled
2085+
// in-process by `attach_resumed_thread_from_browser`).
2086+
if exit_info.start_new_session {
2087+
interactive.resume_picker = false;
2088+
interactive.resume_session_id = None;
2089+
interactive.prompt = exit_info.resume_initial_prompt.clone();
2090+
attempted_repair = false;
2091+
continue;
2092+
}
20822093
if let Some(thread_id) = exit_info.resume_thread_id.clone() {
20832094
interactive.resume_picker = false;
20842095
interactive.resume_session_id = Some(thread_id);
2096+
interactive.prompt = exit_info.resume_initial_prompt.clone();
20852097
attempted_repair = false;
20862098
continue;
20872099
}

codex-rs/tui/src/app.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,12 @@ pub struct AppExitInfo {
382382
/// When set, the outer CLI loop should re-launch the TUI in resume mode
383383
/// targeting this thread id. Populated by the session browser overlay.
384384
pub resume_thread_id: Option<String>,
385+
/// Initial prompt text to inject into the next launch. Populated by the
386+
/// agent view's inline composer.
387+
pub resume_initial_prompt: Option<String>,
388+
/// When true, the outer CLI should launch a fresh session (ignoring any
389+
/// `resume_thread_id`). Triggered by `Ctrl+N` in the agent view.
390+
pub start_new_session: bool,
385391
}
386392

387393
impl AppExitInfo {
@@ -393,6 +399,8 @@ impl AppExitInfo {
393399
update_action: None,
394400
exit_reason: ExitReason::Fatal(message.into()),
395401
resume_thread_id: None,
402+
resume_initial_prompt: None,
403+
start_new_session: false,
396404
}
397405
}
398406
}
@@ -540,6 +548,12 @@ pub(crate) struct App {
540548
/// this thread id after the current TUI exits. Triggered by the session
541549
/// browser overlay.
542550
pub(crate) pending_resume_thread_id: Option<String>,
551+
/// Initial prompt to inject into the resumed (or fresh) session created
552+
/// after this TUI exits. Comes from the agent view's inline composer.
553+
pub(crate) pending_resume_prompt: Option<String>,
554+
/// When true, the outer CLI should launch a brand-new session instead of
555+
/// resuming. `pending_resume_thread_id` is ignored when this is set.
556+
pub(crate) pending_start_new_session: bool,
543557

544558
windows_sandbox: WindowsSandboxState,
545559

@@ -721,6 +735,8 @@ impl App {
721735
update_action: None,
722736
exit_reason: ExitReason::UserRequested,
723737
resume_thread_id: None,
738+
resume_initial_prompt: None,
739+
start_new_session: false,
724740
});
725741
}
726742
};
@@ -965,6 +981,8 @@ See the Codex keymap documentation for supported actions and examples."
965981
pending_update_action: None,
966982
pending_shutdown_exit_thread_id: None,
967983
pending_resume_thread_id: None,
984+
pending_resume_prompt: None,
985+
pending_start_new_session: false,
968986
windows_sandbox: WindowsSandboxState::default(),
969987
thread_event_channels: HashMap::new(),
970988
thread_event_listener_tasks: HashMap::new(),
@@ -1167,6 +1185,8 @@ See the Codex keymap documentation for supported actions and examples."
11671185
update_action: app.pending_update_action,
11681186
exit_reason,
11691187
resume_thread_id: app.pending_resume_thread_id.take(),
1188+
resume_initial_prompt: app.pending_resume_prompt.take(),
1189+
start_new_session: std::mem::take(&mut app.pending_start_new_session),
11701190
})
11711191
}
11721192

codex-rs/tui/src/app/event_dispatch.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,8 +1723,22 @@ impl App {
17231723
AppEvent::SessionBrowserLoaded(sessions) => {
17241724
self.chat_widget.update_session_browser_sessions(sessions);
17251725
}
1726-
AppEvent::ResumeThreadFromBrowser { thread_id } => {
1727-
self.pending_resume_thread_id = Some(thread_id);
1726+
AppEvent::ResumeThreadFromBrowser {
1727+
thread_id,
1728+
initial_prompt,
1729+
} => {
1730+
self.attach_resumed_thread_from_browser(
1731+
tui,
1732+
app_server,
1733+
thread_id,
1734+
initial_prompt,
1735+
)
1736+
.await?;
1737+
}
1738+
AppEvent::StartNewSessionFromBrowser { initial_prompt } => {
1739+
self.pending_start_new_session = true;
1740+
self.pending_resume_thread_id = None;
1741+
self.pending_resume_prompt = initial_prompt;
17281742
return Ok(self
17291743
.handle_exit_mode(app_server, ExitMode::ShutdownFirst)
17301744
.await);

codex-rs/tui/src/app/session_lifecycle.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,49 @@ impl App {
490490
tui.frame_requester().schedule_frame();
491491
}
492492

493+
/// In-process attach to a session selected from the agent view. Calls
494+
/// `app_server.resume_thread` and reuses
495+
/// [`Self::replace_chat_widget_with_app_server_thread`] so the swap happens
496+
/// without exiting the App loop or re-launching the binary — much smoother
497+
/// than the previous re-exec path.
498+
pub(crate) async fn attach_resumed_thread_from_browser(
499+
&mut self,
500+
tui: &mut tui::Tui,
501+
app_server: &mut AppServerSession,
502+
thread_id_str: String,
503+
initial_prompt: Option<String>,
504+
) -> Result<()> {
505+
use codex_protocol::ThreadId;
506+
let thread_id = match ThreadId::from_string(&thread_id_str) {
507+
Ok(id) => id,
508+
Err(err) => {
509+
self.chat_widget
510+
.add_error_message(format!("Invalid thread id `{thread_id_str}`: {err}"));
511+
return Ok(());
512+
}
513+
};
514+
let started = match app_server.resume_thread(self.config.clone(), thread_id).await {
515+
Ok(started) => started,
516+
Err(err) => {
517+
self.chat_widget
518+
.add_error_message(format!("Failed to resume session: {err:#}"));
519+
return Ok(());
520+
}
521+
};
522+
let initial_user_message = crate::chatwidget::create_initial_user_message(
523+
initial_prompt,
524+
Vec::new(),
525+
Vec::new(),
526+
);
527+
self.replace_chat_widget_with_app_server_thread(
528+
tui,
529+
app_server,
530+
started,
531+
initial_user_message,
532+
)
533+
.await
534+
}
535+
493536
pub(super) async fn replace_chat_widget_with_app_server_thread(
494537
&mut self,
495538
tui: &mut tui::Tui,

codex-rs/tui/src/app/startup_prompts.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ pub(super) async fn handle_model_migration_prompt_if_needed(
307307
update_action: None,
308308
exit_reason: ExitReason::UserRequested,
309309
resume_thread_id: None,
310+
resume_initial_prompt: None,
311+
start_new_session: false,
310312
});
311313
}
312314
}

codex-rs/tui/src/app_event.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,17 @@ pub(crate) enum AppEvent {
148148
/// async I/O thread.
149149
SessionBrowserLoaded(Vec<codex_agent_view::SessionSummary>),
150150
/// User picked a session row in the browser; ask the outer loop to exit
151-
/// and re-launch into resume mode for the given thread id.
151+
/// and re-launch into resume mode for the given thread id. When the
152+
/// composer at the bottom of the browser is non-empty, that text is
153+
/// forwarded as the initial prompt for the resumed session.
152154
ResumeThreadFromBrowser {
153155
thread_id: String,
156+
initial_prompt: Option<String>,
157+
},
158+
/// User pressed `Ctrl+N` in the browser. Exit and re-launch a brand-new
159+
/// session using the typed text (if any) as the initial prompt.
160+
StartNewSessionFromBrowser {
161+
initial_prompt: Option<String>,
154162
},
155163
/// Peek panel finished loading rollout items for the highlighted session.
156164
SessionBrowserPeekLoaded(Box<codex_agent_view::PeekContent>),

0 commit comments

Comments
 (0)