#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InlineMessageKind {
Agent,
Error,
Info,
Policy,
Pty,
Tool,
User,
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlashCommandItem {
pub name: String,
pub description: String,
}
impl SlashCommandItem {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
}
}
}
#[derive(Clone, Debug)]
pub struct InlineListSearchConfig {
pub label: String,
pub placeholder: Option<String>,
}
#[derive(Clone, Debug)]
pub struct SecurePromptConfig {
pub label: String,
pub placeholder: Option<String>,
pub mask_input: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SessionSurface {
#[default]
Auto,
Alternate,
Inline,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyboardProtocolSettings {
pub enabled: bool,
pub mode: String,
pub disambiguate_escape_codes: bool,
pub report_event_types: bool,
pub report_alternate_keys: bool,
pub report_all_keys: bool,
}
impl Default for KeyboardProtocolSettings {
fn default() -> Self {
Self {
enabled: true,
mode: "default".to_owned(),
disambiguate_escape_codes: true,
report_event_types: true,
report_alternate_keys: true,
report_all_keys: false,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UiMode {
#[default]
Full,
Minimal,
Focused,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LayoutModeOverride {
#[default]
Auto,
Compact,
Standard,
Wide,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningDisplayMode {
Always,
#[default]
Toggle,
Hidden,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EditingMode {
#[default]
Edit,
Plan,
}
impl EditingMode {
pub fn next(self) -> Self {
match self {
Self::Edit => Self::Plan,
Self::Plan => Self::Edit,
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::Edit => "Edit",
Self::Plan => "Plan",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WizardModalMode {
MultiStep,
TabbedList,
}
#[derive(Clone, Debug)]
pub struct PlanStep {
pub number: usize,
pub description: String,
pub details: Option<String>,
pub files: Vec<String>,
pub completed: bool,
}
#[derive(Clone, Debug)]
pub struct PlanPhase {
pub name: String,
pub steps: Vec<PlanStep>,
pub completed: bool,
}
#[derive(Clone, Debug)]
pub struct PlanContent {
pub title: String,
pub summary: String,
pub file_path: Option<String>,
pub phases: Vec<PlanPhase>,
pub open_questions: Vec<String>,
pub raw_content: String,
pub total_steps: usize,
pub completed_steps: usize,
}
impl PlanContent {
pub fn from_markdown(title: String, content: &str, file_path: Option<String>) -> Self {
let mut phases = Vec::new();
let mut open_questions = Vec::new();
let mut current_phase: Option<PlanPhase> = None;
let mut total_steps = 0;
let mut completed_steps = 0;
let mut summary = String::new();
for line in content.lines() {
let trimmed = line.trim();
if summary.is_empty() && !trimmed.is_empty() && !trimmed.starts_with('#') {
summary = trimmed.to_string();
continue;
}
if let Some(phase_name) = trimmed.strip_prefix("## ") {
if let Some(phase) = current_phase.take() {
phases.push(phase);
}
current_phase = Some(PlanPhase {
name: phase_name.to_string(),
steps: Vec::new(),
completed: false,
});
continue;
}
if trimmed == "## Open Questions" {
if let Some(phase) = current_phase.take() {
phases.push(phase);
}
continue;
}
if let Some(rest) = trimmed.strip_prefix("[ ] ") {
total_steps += 1;
if let Some(ref mut phase) = current_phase {
phase.steps.push(PlanStep {
number: phase.steps.len() + 1,
description: rest.to_string(),
details: None,
files: Vec::new(),
completed: false,
});
}
continue;
}
if let Some(rest) = trimmed
.strip_prefix("[x] ")
.or_else(|| trimmed.strip_prefix("[X] "))
{
total_steps += 1;
completed_steps += 1;
if let Some(ref mut phase) = current_phase {
phase.steps.push(PlanStep {
number: phase.steps.len() + 1,
description: rest.to_string(),
details: None,
files: Vec::new(),
completed: true,
});
}
continue;
}
if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains('.') {
total_steps += 1;
if let Some(ref mut phase) = current_phase {
let desc = trimmed.split_once('.').map(|x| x.1).unwrap_or("").trim();
phase.steps.push(PlanStep {
number: phase.steps.len() + 1,
description: desc.to_string(),
details: None,
files: Vec::new(),
completed: false,
});
}
continue;
}
if trimmed.starts_with("- (") || trimmed.starts_with("- ?") {
open_questions.push(trimmed.trim_start_matches("- ").to_string());
}
}
if let Some(mut phase) = current_phase.take() {
phase.completed = phase.steps.iter().all(|s| s.completed);
phases.push(phase);
}
for phase in &mut phases {
phase.completed = !phase.steps.is_empty() && phase.steps.iter().all(|s| s.completed);
}
Self {
title,
summary,
file_path,
phases,
open_questions,
raw_content: content.to_string(),
total_steps,
completed_steps,
}
}
pub fn progress_percent(&self) -> u8 {
if self.total_steps == 0 {
0
} else {
((self.completed_steps as f32 / self.total_steps as f32) * 100.0) as u8
}
}
}