use super::effects::{SubmitMessageRequest, TuiEffect};
use super::session_flow::ActiveChat;
use super::slash_registry;
fn defer_submission_while_session_opens(chat: &mut ActiveChat) -> bool {
if chat.opening_session_id().is_none() {
return false;
}
chat.app
.set_status("Session is still opening; message kept in composer".to_owned());
true
}
pub enum RootSubmission {
MessageStaged(bool),
SlashCommand(String),
}
pub fn stage_root_submission(
launch_working_directory: &std::path::Path,
launch_options: super::TuiLaunchOptions,
chat: &mut ActiveChat,
placement: bcode_ipc::PromptPlacement,
) -> RootSubmission {
if chat.opening_session_id().is_some() {
chat.app
.set_status("Session is still opening; message kept in composer".to_owned());
return RootSubmission::MessageStaged(false);
}
let message = chat.app.take_pending_submission();
if slash_registry::slash_command_name(&message).is_some() {
chat.app.clear_pending_submission(&message);
return RootSubmission::SlashCommand(message);
}
chat.app.restore_pending_submission(&message);
chat.app.stage_submission();
RootSubmission::MessageStaged(stage_session_message(
launch_working_directory,
launch_options,
chat,
placement,
))
}
pub fn stage_session_message(
launch_working_directory: &std::path::Path,
launch_options: super::TuiLaunchOptions,
chat: &mut ActiveChat,
placement: bcode_ipc::PromptPlacement,
) -> bool {
if defer_submission_while_session_opens(chat) {
return false;
}
let session_id = chat.attached_session_id();
let message = chat.app.take_pending_submission();
if message.trim().is_empty() {
chat.app.clear_pending_submission(&message);
return false;
}
if slash_registry::slash_command_name(&message).is_some() {
chat.app.clear_pending_submission(&message);
chat.app
.set_status("slash command pending root navigation migration".to_owned());
return false;
}
let agent_id = Some(chat.app.display_agent_id().to_owned());
let draft_provider_plugin_id = session_id
.is_none()
.then(|| {
chat.app
.selected_provider_plugin_id()
.map(ToOwned::to_owned)
})
.flatten();
let draft_model_id = session_id
.is_none()
.then(|| chat.app.selected_model_id().map(ToOwned::to_owned))
.flatten();
chat.start_effect(TuiEffect::SubmitMessage {
request: Box::new(SubmitMessageRequest {
submission_id: chat
.app
.pending_submissions()
.last()
.expect("staged submission")
.id()
.clone(),
session_id,
launch_working_directory: launch_working_directory.to_path_buf(),
message,
placement,
provider_plugin_id: draft_provider_plugin_id,
model_id: draft_model_id,
agent_id,
reasoning_effort: chat.app.reasoning_effort().map(ToOwned::to_owned),
reasoning_summary: chat.app.reasoning_summary().map(ToOwned::to_owned),
reasoning_effort_generation: chat.app.pending_reasoning_effort_generation(),
execution: launch_options.turn_execution_options(),
event_sender: chat.event_sender.clone(),
}),
});
chat.app.set_status("sending messageā¦".to_owned());
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn submission_preserves_observed_connection_for_new_and_existing_sessions() {
use super::super::app::{BmuxApp, DaemonConnectionState};
use super::super::session_flow::{ActiveChat, AgentCatalog, ChatSessionAttachment};
for attached in [false, true] {
for connection in [
DaemonConnectionState::Connected,
DaemonConnectionState::Connecting,
DaemonConnectionState::IdleOffline,
DaemonConnectionState::Unavailable,
] {
let (event_sender, event_receiver) = crate::history_flow::session_stream_channel();
let session_id = attached.then(bcode_session_models::SessionId::new);
let mut chat = ActiveChat {
app: BmuxApp::new_with_history(session_id, &[], &[], false),
agents: AgentCatalog::default(),
attachment: session_id.map_or(ChatSessionAttachment::Draft, |session_id| {
ChatSessionAttachment::Attached { session_id }
}),
event_sender,
event_receiver,
event_task: None,
opening_session_progress: None,
pending_effects: super::super::effects::TuiEffectQueue::default(),
};
chat.app.set_daemon_connection(connection);
chat.app.replace_composer_with("fresh message");
chat.app.stage_submission();
assert!(stage_session_message(
std::path::Path::new("."),
crate::TuiLaunchOptions::default(),
&mut chat,
bcode_ipc::PromptPlacement::FollowUp
));
assert_eq!(chat.app.daemon_connection(), connection);
assert_eq!(chat.app.status(), "sending messageā¦");
assert_eq!(chat.pending_effects.queued_agent_id(), Some("build"));
assert!(chat.pending_effects.queued_execution_options().is_some());
drop(chat);
}
}
}
#[test]
fn opening_session_defers_message_without_consuming_composer_text() {
let (event_sender, event_receiver) = crate::history_flow::session_stream_channel();
let session_id = bcode_session_models::SessionId::new();
let mut chat = super::super::session_flow::ActiveChat {
app: super::super::app::BmuxApp::new_with_history(Some(session_id), &[], &[], false),
agents: super::super::session_flow::AgentCatalog::default(),
attachment: super::super::session_flow::ChatSessionAttachment::Opening {
session_id,
anchor_sequence: None,
},
event_sender,
event_receiver,
event_task: None,
opening_session_progress: None,
pending_effects: super::super::effects::TuiEffectQueue::default(),
};
chat.app.replace_composer_with("message after migration");
chat.app.stage_submission();
assert!(defer_submission_while_session_opens(&mut chat));
assert_eq!(
chat.app.take_pending_submission(),
"message after migration"
);
assert!(chat.app.status().contains("still opening"));
}
#[test]
fn active_plugin_status_does_not_block_ordinary_composer_staging() {
let mut app = super::super::app::BmuxApp::new_with_history(None, &[], &[], false);
app.set_plugin_status(vec![bcode_session_view_models::PluginStatusView {
plugin_id: "bcode.loop".to_owned(),
note_id: "loop-active".to_owned(),
text: "Loop active".to_owned(),
priority: 1,
metadata: std::collections::BTreeMap::new(),
}]);
app.paste_composer_text("manual steering");
app.stage_submission();
assert_eq!(app.take_pending_submission(), "manual steering");
let status = app.plugin_status().next().map(|status| status.text.clone());
drop(app);
assert_eq!(status.as_deref(), Some("Loop active"));
}
}