Keep the composer editable during TUI startup (#38642)

## Why

Configuration and app-server initialization can take time before the main TUI is ready, leaving users unable to begin drafting a prompt.

## What changed

- Show a provisional composer while startup work runs and carry its text, cursor position, paste state, and attachments into the initialized chat.
- Limit the provisional composer to safe editing and cancellation, and quarantine input around session pickers, approvals, and other actionable startup screens.
- Preserve configuration validation before terminal checks and recover terminal state cleanly from startup failures and caught panics.

## Testing

- Cover startup editing, multiline and large pastes, keymaps, cancellation, session-picker handoff, approval boundaries, and draft restoration.
- Verify non-interactive launches report configuration errors before terminal errors.

GitOrigin-RevId: fe04a85cfbbcff21ff87fa81fd17474827858575
This commit is contained in:
Charlie Marsh authored and copyberry committed 2026-08-14 20:30:30 +00:00
1 parent 58d2daba45
commit a0bed4be21
26 files changed
+4348 -1151

No files matched your search

+14 -7
View File
@@ -156,6 +156,7 @@ mod scroll_state;
mod selection_popup_common;
mod selection_row_layout;
mod selection_tabs;
mod startup;
mod textarea;
mod unified_exec_footer;
pub(crate) use feedback_view::FeedbackNoteView;
@@ -194,6 +195,7 @@ pub(crate) enum CancellationEvent {
use crate::bottom_pane::prompt_args::parse_slash_name;
pub(crate) use chat_composer::ChatComposer;
pub(crate) use chat_composer::ChatComposerConfig;
pub(crate) use chat_composer::ComposerDraftSnapshot;
pub(crate) use chat_composer::InputResult;
pub(crate) use chat_composer::QueuedInputAction;
pub(crate) use chat_composer_history::HistoryEntry;
@@ -267,6 +269,14 @@ pub(crate) struct BottomPaneParams {
impl BottomPane {
pub fn new(params: BottomPaneParams) -> Self {
Self::new_with_composer_config(params, ChatComposerConfig::default())
}
/// Construct a bottom pane with explicitly restricted composer behavior.
pub(crate) fn new_with_composer_config(
params: BottomPaneParams,
composer_config: ChatComposerConfig,
) -> Self {
let BottomPaneParams {
app_event_tx,
frame_requester,
@@ -277,12 +287,13 @@ impl BottomPane {
animations_enabled,
skills,
} = params;
let mut composer = ChatComposer::new(
let mut composer = ChatComposer::new_with_config(
has_input_focus,
app_event_tx.clone(),
enhanced_keys_supported,
placeholder_text,
disable_paste_burst,
composer_config,
);
composer.set_frame_requester(frame_requester.clone());
let keymap = RuntimeKeymap::defaults();
@@ -577,7 +588,7 @@ impl BottomPane {
fn record_composer_activity_at(&mut self, now: Instant) {
self.last_composer_activity_at = Some(now);
if !self.delayed_approval_requests.is_empty()
if self.has_pending_approval()
&& let Some(delay) = self.approval_prompt_delay_remaining(now)
{
self.request_redraw_in(delay);
@@ -585,7 +596,7 @@ impl BottomPane {
}
fn maybe_show_delayed_approval_requests_at(&mut self, now: Instant) {
if self.delayed_approval_requests.is_empty() || !self.view_stack.is_empty() {
if !self.has_pending_approval() || !self.view_stack.is_empty() {
return;
}
if let Some(delay) = self.approval_prompt_delay_remaining(now) {
@@ -884,10 +895,6 @@ impl BottomPane {
self.composer.cursor()
}
pub(crate) fn composer_draft_snapshot(&self) -> chat_composer::ComposerDraftSnapshot {
self.composer.draft_snapshot()
}
#[cfg(test)]
pub(crate) fn composer_text_elements(&self) -> Vec<TextElement> {
self.composer.text_elements()