use serde::Deserialize;
use serde::Serialize;
use super::EventMsg;
use super::ModelStepOutcome;
use super::Op;
use super::SessionFileReference;
use super::WebSearchAction;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendCommand {
pub name: String,
pub arguments: String,
pub description: String,
pub requires_idle: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendContribution {
pub capability: String,
pub accepts_file_attachments: bool,
pub count: Option<usize>,
pub commands: Vec<FrontendCommand>,
pub widgets: Vec<FrontendWidget>,
pub references: Vec<FrontendReference>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MiddlewareFeature {
pub id: String,
pub label: String,
pub description: String,
pub required: bool,
pub settings: Vec<FrontendSetting>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendSetting {
pub id: String,
pub label: String,
pub description: String,
pub composer: bool,
#[serde(flatten)]
pub kind: FrontendSettingKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum FrontendSettingKind {
Integer {
min: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<i64>,
step: i64,
},
Select {
options: Vec<FrontendSettingOption>,
#[serde(default, skip_serializing_if = "Option::is_none")]
unset_label: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendSettingOption {
pub value: String,
pub label: String,
pub description: String,
pub symbol: Option<FrontendSymbol>,
pub tone: FrontendTone,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FrontendSettingValue {
Integer(i64),
String(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendReference {
pub trigger: char,
pub value: String,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendWidget {
pub id: String,
pub slot: FrontendSlot,
pub text: String,
pub tone: FrontendTone,
pub symbol: Option<FrontendSymbol>,
pub icon_only: bool,
pub progress: Option<FrontendProgress>,
pub content: Option<FrontendWidgetContent>,
pub action: Option<Op>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendProgress {
pub completed: usize,
pub total: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum FrontendWidgetContent {
Blocks {
title: String,
blocks: Vec<FrontendBlock>,
},
Picker {
title: String,
options: Vec<FrontendPickerOption>,
},
ActionList {
title: String,
items: Vec<FrontendActionListItem>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendSlot {
Header,
ComposerHeader,
ComposerFooter,
MessageActions,
TranscriptTail,
Navigation,
ChatMenu,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendBlock {
pub id: Option<String>,
pub group: Option<String>,
pub update: FrontendBlockUpdate,
pub state: FrontendBlockState,
pub role: FrontendBlockRole,
pub title: String,
pub text: String,
pub symbol: Option<FrontendSymbol>,
pub files: Vec<SessionFileReference>,
pub format: FrontendBlockFormat,
pub tone: FrontendTone,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RenderedBlock {
pub capability: String,
pub block: FrontendBlock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendBlockUpdate {
Replace,
Append,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendBlockState {
Pending,
Complete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendBlockRole {
Activity,
Tool,
WebSearch,
Artifact,
Approval,
Notice,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendBlockFormat {
PlainText,
UnifiedDiff,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendPickerOption {
pub label: String,
pub description: String,
pub detail: String,
pub symbol: Option<FrontendSymbol>,
pub shows_detail: bool,
pub op: Op,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendActionListItem {
pub id: String,
pub text: String,
pub state: FrontendListItemState,
pub actions: Vec<FrontendAction>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendListItemState {
Plain,
Pending,
InProgress,
Completed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendAction {
pub id: String,
pub label: String,
pub symbol: FrontendSymbol,
pub tone: FrontendTone,
pub op: Op,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FrontendPreviewEvent {
pub recorded_at_ms: i64,
pub event: EventMsg,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "frontend_type", rename_all = "snake_case")]
pub enum FrontendEvent {
Render {
capability: String,
block: FrontendBlock,
},
Widget {
capability: String,
item: FrontendWidget,
},
RemoveWidget {
capability: String,
id: String,
},
Picker {
title: String,
options: Vec<FrontendPickerOption>,
},
Preview {
id: String,
title: String,
subtitle: String,
page_id: String,
update: FrontendPreviewUpdate,
events: Vec<FrontendPreviewEvent>,
next: Option<Op>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendPreviewUpdate {
Replace,
Prepend,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendTone {
Neutral,
Success,
Warning,
Error,
}
impl EventMsg {
#[must_use]
pub fn presentation(&self) -> Option<RenderedBlock> {
let block = match self {
Self::Error(error) => FrontendBlock {
id: None,
group: None,
update: FrontendBlockUpdate::Replace,
state: FrontendBlockState::Complete,
role: FrontendBlockRole::Notice,
title: "Error".into(),
text: error.message.clone(),
symbol: None,
files: Vec::new(),
format: FrontendBlockFormat::PlainText,
tone: FrontendTone::Error,
},
Self::Warning(warning) => FrontendBlock {
id: None,
group: None,
update: FrontendBlockUpdate::Replace,
state: FrontendBlockState::Complete,
role: FrontendBlockRole::Notice,
title: "Warning".into(),
text: warning.message.clone(),
symbol: None,
files: Vec::new(),
format: FrontendBlockFormat::PlainText,
tone: FrontendTone::Warning,
},
Self::TurnAborted(turn) => FrontendBlock {
id: None,
group: Some(turn.turn_id.clone()),
update: FrontendBlockUpdate::Replace,
state: FrontendBlockState::Complete,
role: FrontendBlockRole::Notice,
title: "Turn aborted".into(),
text: turn.reason.clone(),
symbol: None,
files: Vec::new(),
format: FrontendBlockFormat::PlainText,
tone: FrontendTone::Warning,
},
Self::ModelStepCompleted(step) if step.outcome == ModelStepOutcome::Retrying => {
FrontendBlock {
id: Some(format!("{}/retry", step.model_step_id)),
group: Some(step.turn_id.clone()),
update: FrontendBlockUpdate::Replace,
state: FrontendBlockState::Complete,
role: FrontendBlockRole::Notice,
title: "Reconnecting…".into(),
text: String::new(),
symbol: None,
files: Vec::new(),
format: FrontendBlockFormat::PlainText,
tone: FrontendTone::Warning,
}
}
Self::WebSearchBegin(search) => FrontendBlock {
id: Some(format!("{}/{}", search.model_step_id, search.call_id)),
group: Some(search.turn_id.clone()),
update: FrontendBlockUpdate::Replace,
state: FrontendBlockState::Pending,
role: FrontendBlockRole::WebSearch,
title: "Searching the web".into(),
text: String::new(),
symbol: Some(FrontendSymbol::Search),
files: Vec::new(),
format: FrontendBlockFormat::PlainText,
tone: FrontendTone::Neutral,
},
Self::WebSearchEnd(search) => {
let (title, text, tone) = match &search.action {
WebSearchAction::Search { queries } => (
"Searched the web",
queries.join("\n"),
FrontendTone::Success,
),
WebSearchAction::OpenPage { url } => (
"Opened a web page",
url.clone().unwrap_or_default(),
FrontendTone::Success,
),
WebSearchAction::FindInPage { url, pattern } => {
let text = match (url, pattern) {
(Some(url), Some(pattern)) => format!("{pattern}\n{url}"),
(Some(url), None) => url.clone(),
(None, Some(pattern)) => pattern.clone(),
(None, None) => String::new(),
};
("Searched a web page", text, FrontendTone::Success)
}
WebSearchAction::Interrupted => (
"Web search interrupted",
String::new(),
FrontendTone::Warning,
),
WebSearchAction::Other => {
("Web search complete", String::new(), FrontendTone::Success)
}
};
FrontendBlock {
id: Some(format!("{}/{}", search.model_step_id, search.call_id)),
group: Some(search.turn_id.clone()),
update: FrontendBlockUpdate::Replace,
state: FrontendBlockState::Complete,
role: FrontendBlockRole::WebSearch,
title: title.into(),
text,
symbol: Some(FrontendSymbol::Search),
files: Vec::new(),
format: FrontendBlockFormat::PlainText,
tone,
}
}
Self::Frontend(FrontendEvent::Render { capability, block }) => {
return Some(RenderedBlock {
capability: capability.clone(),
block: block.clone(),
});
}
_ => return None,
};
Some(RenderedBlock {
capability: match self {
Self::WebSearchBegin(_) | Self::WebSearchEnd(_) => "web_search",
_ => "agent",
}
.into(),
block,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrontendSymbol {
Agent,
Brain,
Branch,
Chat,
Delete,
Edit,
Promote,
Route,
Search,
Shield,
ShieldAlert,
ShieldCheck,
ShieldOff,
Sparkle,
Storage,
Task,
Custom(String),
}
impl FrontendSymbol {
pub fn as_str(&self) -> &str {
match self {
Self::Agent => "agent",
Self::Brain => "brain",
Self::Branch => "branch",
Self::Chat => "chat",
Self::Delete => "delete",
Self::Edit => "edit",
Self::Promote => "promote",
Self::Route => "route",
Self::Search => "search",
Self::Shield => "shield",
Self::ShieldAlert => "shield_alert",
Self::ShieldCheck => "shield_check",
Self::ShieldOff => "shield_off",
Self::Sparkle => "sparkle",
Self::Storage => "storage",
Self::Task => "task",
Self::Custom(name) => name,
}
}
pub(crate) fn from_wire(name: &str) -> Self {
match name {
"agent" => Self::Agent,
"brain" => Self::Brain,
"branch" => Self::Branch,
"chat" => Self::Chat,
"delete" => Self::Delete,
"edit" => Self::Edit,
"promote" => Self::Promote,
"route" => Self::Route,
"search" => Self::Search,
"shield" => Self::Shield,
"shield_alert" => Self::ShieldAlert,
"shield_check" => Self::ShieldCheck,
"shield_off" => Self::ShieldOff,
"sparkle" => Self::Sparkle,
"storage" => Self::Storage,
"task" => Self::Task,
other => Self::Custom(other.to_owned()),
}
}
}
impl std::fmt::Display for FrontendSymbol {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
impl Serialize for FrontendSymbol {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for FrontendSymbol {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
String::deserialize(deserializer).map(|name| Self::from_wire(&name))
}
}