magi-code 0.80.0

Repository-aware CLI coding agent for terminal work
Documentation
use super::{MissionControlApp, actions::ActionContext, display_reducer};
use crate::tui::{input, state};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

pub(super) fn skill_candidates(
    candidates: &[state::AutocompleteCandidate],
) -> Vec<state::AutocompleteCandidate> {
    candidates
        .iter()
        .filter(|candidate| candidate.kind == state::AutocompleteKind::SkillTag)
        .cloned()
        .collect()
}

impl MissionControlApp {
    pub(super) fn edit_subagent_prompt(
        &mut self,
        key: KeyEvent,
        viewport: input::PromptViewport,
        ui_state: &mut state::MissionControlState,
        context: &mut ActionContext<'_>,
    ) {
        let Some(control) = ui_state
            .selected_subagent_control()
            .filter(|control| control.is_active())
        else {
            return;
        };
        let candidates = skill_candidates(context.autocomplete_candidates);
        let copy = ui_state
            .with_selected_subagent_prompt(|state| {
                if key.code == KeyCode::Enter
                    && key.modifiers == KeyModifiers::NONE
                    && !state.autocomplete_visible()
                {
                    submit_steering(state, &control.steering);
                    return None;
                }
                edit_with_shared_prompt_keys(key, viewport, state, &candidates)
            })
            .flatten();
        if let Some(text) = copy {
            Self::apply_copy_request(
                &text,
                ui_state,
                context.clipboard,
                std::time::Instant::now(),
            );
        }
    }
}

fn edit_with_shared_prompt_keys(
    key: KeyEvent,
    viewport: input::PromptViewport,
    state: &mut state::MissionControlState,
    candidates: &[state::AutocompleteCandidate],
) -> Option<String> {
    let viewer = state.modals.subagent_viewer.take();
    let command = input::classify_key_with_all_viewports(
        key,
        state,
        input::KeyViewports {
            prompt: Some(viewport),
            ..Default::default()
        },
        candidates,
    );
    state.modals.subagent_viewer = viewer;
    let (action, display) = command.into_parts();
    if let Some(display) = display {
        apply_editor_display(display, state, candidates);
    }
    match action {
        input::InputAction::CopySelectedPrompt(text) => Some(text),
        _ => None,
    }
}

// A positive allow-list prevents primary navigation, command dispatch, history,
// shutdown, or bash-mode submission from escaping the shared key classifier.
fn apply_editor_display(
    command: input::DisplayCommand,
    state: &mut state::MissionControlState,
    candidates: &[state::AutocompleteCandidate],
) {
    match command {
        input::DisplayCommand::Batch(commands) => {
            for command in commands {
                apply_editor_display(command, state, candidates);
            }
        }
        input::DisplayCommand::Prompt(input::PromptCommand::TakeForSubmit) => {}
        command @ (input::DisplayCommand::Prompt(_)
        | input::DisplayCommand::RefreshAutocomplete) => {
            display_reducer::apply_display_command(command, state, candidates);
        }
        _ => {}
    }
}

fn submit_steering(
    state: &mut state::MissionControlState,
    steering: &crate::agent::steering::AgentSteering,
) {
    use crate::agent::steering::SteeringRejected;
    let prompt = state.prompt_plain_text();
    if prompt.trim_start().starts_with('/') {
        state.status = "Slash commands are not available for subagents".into();
        return;
    }
    if prompt.trim().is_empty() {
        return;
    }
    // A leading ! is ordinary steering text, never a shell invocation.
    match steering.try_enqueue_nonblocking(prompt) {
        Ok(_) => {
            state.clear_prompt_input();
            state.hide_autocomplete();
            state.status = "Subagent steering queued".into();
        }
        Err(SteeringRejected::Full { capacity }) => {
            state.status = format!("Steering queue full ({capacity} max)");
        }
        Err(SteeringRejected::Closed) => {
            state.status = "Subagent no longer accepts steering".into();
        }
        Err(SteeringRejected::Busy) => {
            state.status = "Steering is being saved; try again".into();
        }
        Err(SteeringRejected::Empty) => {}
    }
}

pub(super) fn cancel_or_recall_selected(state: &mut state::MissionControlState) {
    let Some(control) = state.selected_subagent_control() else {
        return;
    };
    if !recall_selected_steering(state, &control.steering) && control.is_active() {
        control.cancel();
        state.status = "Subagent cancellation requested".into();
    }
    state.set_subagent_prompt_notice(state.status.clone());
}

fn recall_selected_steering(
    state: &mut state::MissionControlState,
    steering: &crate::agent::steering::AgentSteering,
) -> bool {
    let mut recall_rejected = false;
    let recalled = steering.restore_pending_messages(|messages| {
        let restored = state
            .with_selected_subagent_prompt(|state| {
                let draft = state.prompt_plain_text();
                let text = if draft.is_empty() {
                    messages.to_owned()
                } else {
                    format!("{messages}\n{draft}")
                };
                let restored = matches!(
                    state.set_prompt_text(&text, text.len()),
                    state::PromptEditResult::Changed | state::PromptEditResult::Unchanged
                );
                if restored {
                    state.hide_autocomplete();
                }
                restored
            })
            .unwrap_or(false);
        recall_rejected = !restored;
        restored
    });
    state.status = match recalled {
        Ok(_) if recall_rejected => "Subagent draft is too large to recall steering".into(),
        Ok(true) => "Queued steering restored to subagent prompt".into(),
        Ok(false) => return false,
        Err(()) => "Steering is being injected; try Alt-C again".into(),
    };
    true
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn subagent_prompt_recall_preserves_draft_primary_and_other_queues() {
        use crate::output::{
            ActivityEvent, ActivityId, ActivityKind, ActivityMetadata, ActivityStatus,
        };
        let mut state = state::MissionControlState::default();
        let batch = ActivityId::new("batch");
        let task = batch.child("child");
        for (id, parent_id, kind) in [
            (batch.clone(), None, ActivityKind::SubagentBatch),
            (
                task.clone(),
                Some(batch.clone()),
                ActivityKind::SubagentTask,
            ),
        ] {
            state.apply_activity_event(ActivityEvent::Started {
                id,
                parent_id,
                kind,
                status: ActivityStatus::Running,
                metadata: ActivityMetadata::new("test"),
            });
        }
        state.set_prompt_text("primary draft", 13);
        assert!(state.open_subagent_viewer(batch, task));
        state.with_selected_subagent_prompt(|state| {
            state.set_prompt_text("child draft", 11);
        });
        let selected = crate::agent::steering::AgentSteering::default();
        let other = crate::agent::steering::AgentSteering::default();
        selected.try_enqueue("queued update".into()).unwrap();
        other.try_enqueue("other child".into()).unwrap();
        assert!(recall_selected_steering(&mut state, &selected));
        assert_eq!(
            state.selected_subagent_prompt().unwrap().editor.text(),
            "queued update\nchild draft"
        );
        assert_eq!(state.prompt_plain_text(), "primary draft");
        assert_eq!(selected.pending_count(), 0);
        assert_eq!(other.pending_count(), 1);
        assert!(!recall_selected_steering(&mut state, &selected));
        selected.try_enqueue("must remain queued".into()).unwrap();
        state.with_selected_subagent_prompt(|state| {
            state.set_prompt_text(&"x".repeat(state::MAX_PROMPT_BYTES), 0);
        });
        assert!(recall_selected_steering(&mut state, &selected));
        assert_eq!(selected.pending_count(), 1);
        assert!(state.status.contains("too large"));
        selected.close();
        state.with_selected_subagent_prompt(|state| {
            state.clear_prompt_input();
        });
        assert!(!state.subagent_prompt_editable());
        assert!(recall_selected_steering(&mut state, &selected));
        assert_eq!(
            state.selected_subagent_prompt().unwrap().editor.text(),
            "must remain queued"
        );
        state.with_selected_subagent_prompt(|state| submit_steering(state, &selected));
        assert_eq!(
            state.selected_subagent_prompt().unwrap().editor.text(),
            "must remain queued"
        );
        assert_eq!(selected.pending_count(), 0);
    }

    #[test]
    fn subagent_prompt_shared_keys_complete_skills_and_keep_editor_undo() {
        let mut state = state::MissionControlState::default();
        let candidates = skill_candidates(&[
            state::AutocompleteCandidate::skill_tag("rust-dev"),
            state::AutocompleteCandidate::slash_command("quit", "exit"),
        ]);
        let viewport = input::PromptViewport {
            visible_rows: 3,
            wrap_width: 40,
        };
        for ch in "$rust".chars() {
            edit_with_shared_prompt_keys(
                KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE),
                viewport,
                &mut state,
                &candidates,
            );
        }
        assert!(state.autocomplete_visible());
        edit_with_shared_prompt_keys(
            KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE),
            viewport,
            &mut state,
            &candidates,
        );
        assert_eq!(state.prompt_plain_text().trim(), "$rust-dev");
        edit_with_shared_prompt_keys(
            KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL),
            viewport,
            &mut state,
            &candidates,
        );
        assert!(state.prompt_is_empty());
        edit_with_shared_prompt_keys(
            KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL),
            viewport,
            &mut state,
            &candidates,
        );
        assert_eq!(state.prompt_plain_text().trim(), "$rust-dev");
        state.set_prompt_text("/qu", 3);
        state.recompute_autocomplete(&candidates);
        assert!(!state.autocomplete_visible());
    }

    #[test]
    fn subagent_prompt_submission_rejects_commands_and_never_executes_bash() {
        let steering = crate::agent::steering::AgentSteering::default();
        let mut state = state::MissionControlState::default();
        state.set_prompt_text(" /quit", 6);
        submit_steering(&mut state, &steering);
        assert_eq!(state.prompt_plain_text(), " /quit");
        assert_eq!(steering.pending_count(), 0);
        assert!(state.status.contains("Slash commands"));
        state.set_prompt_text("!echo hello", 11);
        submit_steering(&mut state, &steering);
        assert!(state.prompt_is_empty());
        assert!(steering.drain_collapsed().unwrap().contains("!echo hello"));
        steering.close();
        state.set_prompt_text("keep this", 9);
        submit_steering(&mut state, &steering);
        assert_eq!(state.prompt_plain_text(), "keep this");
    }
}