use std::path::PathBuf;
use crate::app::McpServerConfig;
use crate::app::instructions::LoadedInstructions;
use crate::models::tool_call::ToolCall as ModelToolCall;
use crate::models::{ReasoningChunk, ReasoningLevel, TokenUsage, UserFacingError};
use super::ids::{ToolCallId, TurnId};
use super::runtime::RuntimeSignal;
use super::state::ContextUsageSnapshot;
use super::state::StatusKind;
use super::state::{ConversationSummary, McpToolSpec, ToolOutcome};
use super::{CompactionResult, CompactionTrigger};
#[derive(Debug, Clone)]
pub enum Msg {
Key(Key),
Paste(Paste),
SubmitPrompt {
text: String,
attachment_ids: Vec<u64>,
},
Slash(SlashCmd),
CancelTurn,
ConfirmAccepted,
ConfirmDeclined,
Quit,
RuntimeSignal(RuntimeSignal),
StreamText {
turn: TurnId,
chunk: String,
},
StreamReasoning {
turn: TurnId,
chunk: ReasoningChunk,
},
StreamToolCall {
turn: TurnId,
call: ModelToolCall,
},
ContextUsageEstimated {
turn: TurnId,
snapshot: ContextUsageSnapshot,
},
CompactionFinished {
turn: TurnId,
result: CompactionResult,
},
CompactionFailed {
turn: TurnId,
trigger: CompactionTrigger,
message: String,
kind: StatusKind,
},
StreamDone {
turn: TurnId,
usage: Option<TokenUsage>,
thinking_signature: Option<String>,
},
UpstreamError {
turn: TurnId,
error: UserFacingError,
},
TurnCancelled(TurnId),
ToolStarted {
turn: TurnId,
call_id: ToolCallId,
},
ToolProgress {
turn: TurnId,
call_id: ToolCallId,
event: crate::providers::ProgressEvent,
},
ToolFinished {
turn: TurnId,
call_id: ToolCallId,
outcome: ToolOutcome,
},
McpServerReady {
name: String,
tools: Vec<McpToolSpec>,
},
McpServerErrored {
name: String,
reason: String,
},
McpServerStopped {
name: String,
},
InstructionsChanged(Option<LoadedInstructions>),
SessionSaved,
ConversationLoaded(crate::session::ConversationHistory),
ConversationsListed(Vec<ConversationSummary>),
ModelPullFinished {
model: String,
},
ModelPullProgress(String),
Tick,
StatusDismiss,
Resize {
width: u16,
height: u16,
},
TransientStatus {
text: String,
kind: super::state::StatusKind,
dismiss_ms: u64,
},
MouseScroll {
delta: i16,
},
OpenImageAt {
message_index: usize,
image_index: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Key {
pub code: KeyCode,
pub modifiers: KeyMods,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyCode {
Char(char),
Enter,
Escape,
Backspace,
Delete,
Tab,
BackTab,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
F(u8),
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct KeyMods {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
}
impl KeyMods {
pub const NONE: Self = Self {
ctrl: false,
alt: false,
shift: false,
};
pub const fn ctrl() -> Self {
Self {
ctrl: true,
..Self::NONE
}
}
pub const fn alt() -> Self {
Self {
alt: true,
..Self::NONE
}
}
pub fn is_empty(self) -> bool {
!self.ctrl && !self.alt && !self.shift
}
}
#[derive(Debug, Clone)]
pub enum Paste {
Text(String),
Image { bytes: Vec<u8>, format: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SlashCmd {
Model(Option<String>),
Reasoning(Option<ReasoningLevel>),
Clear,
Save(Option<String>),
Load(Option<String>),
List,
Usage,
Context,
Compact(Option<String>),
CloudSetup,
Help,
Quit,
Unknown(String),
}
impl Msg {
pub fn turn_id(&self) -> Option<TurnId> {
match self {
Msg::StreamText { turn, .. }
| Msg::StreamReasoning { turn, .. }
| Msg::StreamToolCall { turn, .. }
| Msg::ContextUsageEstimated { turn, .. }
| Msg::CompactionFinished { turn, .. }
| Msg::CompactionFailed { turn, .. }
| Msg::StreamDone { turn, .. }
| Msg::UpstreamError { turn, .. }
| Msg::ToolStarted { turn, .. }
| Msg::ToolProgress { turn, .. }
| Msg::ToolFinished { turn, .. } => Some(*turn),
Msg::TurnCancelled(turn) => Some(*turn),
_ => None,
}
}
pub fn kind(&self) -> MsgKind {
match self {
Msg::Key(_) => MsgKind::Key,
Msg::Paste(_) => MsgKind::Paste,
Msg::SubmitPrompt { .. } => MsgKind::SubmitPrompt,
Msg::Slash(_) => MsgKind::Slash,
Msg::CancelTurn => MsgKind::CancelTurn,
Msg::ConfirmAccepted | Msg::ConfirmDeclined => MsgKind::Confirm,
Msg::Quit => MsgKind::Quit,
Msg::RuntimeSignal(_) => MsgKind::RuntimeSignal,
Msg::StreamText { .. } => MsgKind::StreamText,
Msg::StreamReasoning { .. } => MsgKind::StreamReasoning,
Msg::StreamToolCall { .. } => MsgKind::StreamToolCall,
Msg::ContextUsageEstimated { .. } => MsgKind::ContextUsageEstimated,
Msg::CompactionFinished { .. } => MsgKind::CompactionFinished,
Msg::CompactionFailed { .. } => MsgKind::CompactionFailed,
Msg::StreamDone { .. } => MsgKind::StreamDone,
Msg::UpstreamError { .. } => MsgKind::UpstreamError,
Msg::ToolStarted { .. } => MsgKind::ToolStarted,
Msg::ToolProgress { .. } => MsgKind::ToolProgress,
Msg::ToolFinished { .. } => MsgKind::ToolFinished,
Msg::TurnCancelled(_) => MsgKind::TurnCancelled,
Msg::McpServerReady { .. }
| Msg::McpServerErrored { .. }
| Msg::McpServerStopped { .. } => MsgKind::Mcp,
Msg::InstructionsChanged(_) => MsgKind::InstructionsChanged,
Msg::SessionSaved => MsgKind::SessionSaved,
Msg::ConversationLoaded(_) => MsgKind::ConversationLoaded,
Msg::ConversationsListed(_) => MsgKind::ConversationsListed,
Msg::ModelPullFinished { .. } => MsgKind::ModelPullFinished,
Msg::ModelPullProgress(_) => MsgKind::ModelPullProgress,
Msg::Tick => MsgKind::Tick,
Msg::StatusDismiss => MsgKind::StatusDismiss,
Msg::Resize { .. } => MsgKind::Resize,
Msg::MouseScroll { .. } => MsgKind::MouseScroll,
Msg::OpenImageAt { .. } => MsgKind::OpenImageAt,
Msg::TransientStatus { .. } => MsgKind::TransientStatus,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MsgKind {
Key,
Paste,
SubmitPrompt,
Slash,
CancelTurn,
Confirm,
Quit,
RuntimeSignal,
StreamText,
StreamReasoning,
StreamToolCall,
ContextUsageEstimated,
CompactionFinished,
CompactionFailed,
StreamDone,
UpstreamError,
ToolStarted,
ToolProgress,
ToolFinished,
TurnCancelled,
Mcp,
InstructionsChanged,
SessionSaved,
ConversationLoaded,
ConversationsListed,
ModelPullFinished,
ModelPullProgress,
Tick,
StatusDismiss,
Resize,
MouseScroll,
OpenImageAt,
TransientStatus,
}
#[derive(Debug, Clone)]
pub struct StartupConfig {
pub mcp_servers: std::collections::HashMap<String, McpServerConfig>,
pub cwd: PathBuf,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn turn_id_extracted_from_stream_messages() {
let m = Msg::StreamText {
turn: TurnId(7),
chunk: "hi".to_string(),
};
assert_eq!(m.turn_id(), Some(TurnId(7)));
}
#[test]
fn turn_id_none_for_user_intent() {
let m = Msg::CancelTurn;
assert_eq!(m.turn_id(), None);
let m = Msg::Quit;
assert_eq!(m.turn_id(), None);
let m = Msg::Tick;
assert_eq!(m.turn_id(), None);
}
#[test]
fn turn_id_none_for_mcp_lifecycle() {
let m = Msg::McpServerReady {
name: "s".to_string(),
tools: vec![],
};
assert_eq!(m.turn_id(), None);
}
#[test]
fn key_mods_builder_defaults_match_const() {
assert_eq!(KeyMods::default(), KeyMods::NONE);
assert!(KeyMods::ctrl().ctrl);
assert!(!KeyMods::ctrl().alt);
assert!(!KeyMods::ctrl().shift);
}
#[test]
fn kind_stable_across_variants() {
assert_eq!(Msg::Quit.kind(), MsgKind::Quit);
assert_eq!(Msg::Tick.kind(), MsgKind::Tick);
assert_eq!(
Msg::StreamText {
turn: TurnId(1),
chunk: String::new()
}
.kind(),
MsgKind::StreamText
);
}
#[test]
fn slash_cmd_carries_none_for_no_arg() {
let c = SlashCmd::Model(None);
assert_eq!(c, SlashCmd::Model(None));
assert_ne!(c, SlashCmd::Model(Some("ollama/qwen3".to_string())));
}
}