use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::app::McpServerConfig;
use crate::app::instructions::LoadedInstructions;
use crate::models::tool_call::ToolCall as ModelToolCall;
use crate::models::{
FinishReason, ProviderContinuation, ReasoningChunk, ReasoningLevel, TokenUsage, UserFacingError,
};
use crate::runtime::{
ApprovalRecord, CheckpointRecord, PluginInstallRecord, ProcessRecord, SafetyMode, TaskRecord,
TaskTimelineEvent,
};
use super::ids::{ToolCallId, TurnId};
use super::question::Question;
use super::runtime::RuntimeSignal;
use super::state::ContextUsageSnapshot;
use super::state::StatusKind;
use super::state::{ApprovalKind, ConversationSummary, McpToolSpec, ToolOutcome};
use super::{CompactionResult, CompactionTrigger};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum Msg {
Key(Key),
Paste(Paste),
ClipboardRead(ClipboardRead),
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,
},
ProviderContextResolved {
model_id: String,
model_max: Option<usize>,
effective: Option<usize>,
source: Option<crate::models::adapters::ollama_sizing::NumCtxSource>,
#[serde(default)]
max_output: Option<usize>,
},
OllamaPlacementResolved {
model_id: String,
size_vram_bytes: u64,
total_bytes: u64,
suggested_num_ctx: Option<u32>,
},
ProviderVisionResolved {
model_id: String,
supports_vision: Option<bool>,
warn: bool,
},
BuiltinToolSchemaTokens(usize),
CompactionFinished {
turn: TurnId,
result: CompactionResult,
},
CompactionFailed {
turn: TurnId,
trigger: CompactionTrigger,
message: String,
kind: StatusKind,
},
StreamDone {
turn: TurnId,
usage: Option<TokenUsage>,
provider_continuation: Option<ProviderContinuation>,
stop_reason: Option<FinishReason>,
},
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,
},
ApprovalRequested {
turn: TurnId,
call_id: ToolCallId,
tool: String,
risk: String,
kind: ApprovalKind,
prompt: String,
allowlist_scope: String,
},
QuestionAsked {
turn: TurnId,
call_id: ToolCallId,
questions: Vec<Question>,
},
TasksUpdated {
store: crate::domain::tasks::TaskStore,
},
TaskNotice {
text: String,
},
McpServerReady {
name: String,
tools: Vec<McpToolSpec>,
},
McpServerErrored {
name: String,
reason: String,
},
McpServerStopped {
name: String,
},
HookContext {
turn: TurnId,
texts: Vec<String>,
},
InstructionsChanged(Option<LoadedInstructions>),
MemoryChanged(Option<crate::app::memory::LoadedMemory>),
SessionSaved,
ConversationLoaded(crate::session::ConversationHistory),
ConversationsListed(Vec<ConversationSummary>),
ProjectFilesListed(Vec<String>),
ScratchpadReady {
session_id: String,
path: PathBuf,
},
RuntimeTasksListed(Vec<TaskRecord>),
RuntimeTaskLoaded {
task: Option<TaskRecord>,
events: Vec<TaskTimelineEvent>,
},
RuntimeProcessesListed(Vec<ProcessRecord>),
RuntimeText(String),
RuntimeApprovalsListed(Vec<ApprovalRecord>),
RuntimeCheckpointsListed(Vec<CheckpointRecord>),
ForkCheckpointsFound(Vec<CheckpointRecord>),
RuntimePluginsListed(Vec<PluginInstallRecord>),
ModelPullFinished {
model: String,
},
ModelPullProgress(String),
Tick,
Resize {
width: u16,
height: u16,
},
TransientStatus {
text: String,
},
EditorReturned {
text: Option<String>,
},
BackgroundAgentStarted {
agent_id: String,
description: String,
},
BackgroundAgentProgress {
agent_id: String,
activity: String,
tokens: usize,
},
BackgroundAgentFinished {
agent_id: String,
description: String,
report: String,
success: bool,
#[serde(default)]
cancelled: bool,
usage: Option<TokenUsage>,
tokens: usize,
duration_secs: u64,
},
MouseScroll {
delta: i16,
},
FocusChanged(bool),
OpenImageAt {
message_index: usize,
image_index: usize,
#[serde(default)]
image_number: Option<u64>,
},
CopySelection(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Key {
pub code: KeyCode,
pub modifiers: KeyMods,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
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, Serialize, Deserialize)]
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, Serialize, Deserialize)]
pub enum Paste {
Text(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClipboardRead {
Image {
#[serde(with = "crate::utils::serde_base64")]
bytes: Vec<u8>,
format: String,
},
Text(String),
Empty,
Error(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ContextCmd {
Show,
Set(u32),
Auto,
Max,
Offload(bool),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SlashCmd {
Model(Option<String>),
Reasoning(Option<ReasoningLevel>),
VisibleReasoning(Option<String>),
Safety(Option<SafetyMode>),
Plan(Option<String>),
Config,
Clear,
Save(Option<String>),
Load(Option<String>),
List,
Usage,
Todos(Option<String>),
Scratchpad,
Context(ContextCmd),
Compact(Option<String>),
Memory,
Remember(Option<String>),
Forget(Option<String>),
ConsolidateMemory,
Doctor,
Tasks,
Task(Option<String>),
Pause(Option<String>),
Resume(Option<String>),
Cancel(Option<String>),
Handoff(Option<String>),
Report(Option<String>),
Processes,
Agents(Option<String>),
Logs(Option<String>),
Stop(Option<String>),
Restart(Option<String>),
Open(Option<String>),
Ports,
Approvals,
Approve(Option<String>),
Deny(Option<String>),
Checkpoint(Option<String>),
Checkpoints,
Restore(Option<String>),
ModelInfo(Option<String>),
Plugins,
CloudSetup,
Theme(Option<String>),
Editor,
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, .. }
| Msg::ApprovalRequested { turn, .. }
| Msg::QuestionAsked { turn, .. }
| Msg::HookContext { 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::ClipboardRead(_) => MsgKind::ClipboardRead,
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::ProviderContextResolved { .. } => MsgKind::ProviderContextResolved,
Msg::OllamaPlacementResolved { .. } => MsgKind::OllamaPlacementResolved,
Msg::ProviderVisionResolved { .. } => MsgKind::ProviderVisionResolved,
Msg::BuiltinToolSchemaTokens(_) => MsgKind::BuiltinToolSchemaTokens,
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::ApprovalRequested { .. } => MsgKind::ApprovalRequested,
Msg::QuestionAsked { .. } => MsgKind::QuestionAsked,
Msg::TasksUpdated { .. } => MsgKind::TasksUpdated,
Msg::TaskNotice { .. } => MsgKind::TaskNotice,
Msg::TurnCancelled(_) => MsgKind::TurnCancelled,
Msg::McpServerReady { .. }
| Msg::McpServerErrored { .. }
| Msg::McpServerStopped { .. } => MsgKind::Mcp,
Msg::HookContext { .. } => MsgKind::HookContext,
Msg::InstructionsChanged(_) => MsgKind::InstructionsChanged,
Msg::MemoryChanged(_) => MsgKind::MemoryChanged,
Msg::SessionSaved => MsgKind::SessionSaved,
Msg::ConversationLoaded(_) => MsgKind::ConversationLoaded,
Msg::ConversationsListed(_) => MsgKind::ConversationsListed,
Msg::ProjectFilesListed(_) => MsgKind::ProjectFilesListed,
Msg::ScratchpadReady { .. } => MsgKind::ScratchpadReady,
Msg::RuntimeTasksListed(_)
| Msg::RuntimeTaskLoaded { .. }
| Msg::RuntimeProcessesListed(_)
| Msg::RuntimeText(_)
| Msg::RuntimeApprovalsListed(_)
| Msg::RuntimeCheckpointsListed(_)
| Msg::ForkCheckpointsFound(_)
| Msg::RuntimePluginsListed(_) => MsgKind::RuntimeStore,
Msg::ModelPullFinished { .. } => MsgKind::ModelPullFinished,
Msg::ModelPullProgress(_) => MsgKind::ModelPullProgress,
Msg::Tick => MsgKind::Tick,
Msg::Resize { .. } => MsgKind::Resize,
Msg::MouseScroll { .. } => MsgKind::MouseScroll,
Msg::FocusChanged(_) => MsgKind::FocusChanged,
Msg::OpenImageAt { .. } => MsgKind::OpenImageAt,
Msg::TransientStatus { .. } => MsgKind::TransientStatus,
Msg::EditorReturned { .. } => MsgKind::EditorReturned,
Msg::BackgroundAgentStarted { .. }
| Msg::BackgroundAgentProgress { .. }
| Msg::BackgroundAgentFinished { .. } => MsgKind::BackgroundAgent,
Msg::CopySelection(_) => MsgKind::CopySelection,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MsgKind {
Key,
Paste,
ClipboardRead,
SubmitPrompt,
Slash,
CancelTurn,
Confirm,
Quit,
RuntimeSignal,
StreamText,
StreamReasoning,
StreamToolCall,
ContextUsageEstimated,
ProviderContextResolved,
OllamaPlacementResolved,
ProviderVisionResolved,
BuiltinToolSchemaTokens,
CompactionFinished,
CompactionFailed,
StreamDone,
UpstreamError,
ToolStarted,
ToolProgress,
ToolFinished,
ApprovalRequested,
QuestionAsked,
TasksUpdated,
TaskNotice,
TurnCancelled,
Mcp,
InstructionsChanged,
HookContext,
MemoryChanged,
SessionSaved,
ConversationLoaded,
ConversationsListed,
ProjectFilesListed,
ScratchpadReady,
RuntimeStore,
ModelPullFinished,
ModelPullProgress,
Tick,
Resize,
MouseScroll,
FocusChanged,
BackgroundAgent,
OpenImageAt,
TransientStatus,
EditorReturned,
CopySelection,
}
#[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())));
}
}