use crate::session::event::ReactionOutcome;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChatChar {
pub codepoints: &'static [&'static str],
pub name: &'static str,
pub meaning: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReactionKey {
Accepted,
Succeeded,
Failed,
Interrupted,
}
impl ReactionKey {
pub fn entry(self) -> ChatChar {
match self {
ReactionKey::Accepted => ChatChar {
codepoints: &["U+23F3"],
name: "hourglass not done",
meaning: "accepted, queued or running",
},
ReactionKey::Succeeded => ChatChar {
codepoints: &["U+2705"],
name: "white heavy check mark",
meaning: "the turn it started completed",
},
ReactionKey::Failed => ChatChar {
codepoints: &["U+274C"],
name: "cross mark",
meaning: "the turn failed, or the message was rejected",
},
ReactionKey::Interrupted => ChatChar {
codepoints: &["U+23F9", "U+FE0F"],
name: "stop button",
meaning: "the turn was interrupted",
},
}
}
pub fn glyph(self) -> String {
glyph(&self.entry())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrefixKey {
Tool,
#[allow(
dead_code,
reason = "the tests walk the whole table, which is the point of enumerating it"
)]
Thinking,
Question,
Warning,
Delegated,
Connection,
}
#[allow(
dead_code,
reason = "the tests walk the whole table, which is the point of enumerating it"
)]
pub const ALL_PREFIXES: [PrefixKey; 6] = [
PrefixKey::Tool,
PrefixKey::Thinking,
PrefixKey::Question,
PrefixKey::Warning,
PrefixKey::Delegated,
PrefixKey::Connection,
];
impl PrefixKey {
pub fn entry(self) -> ChatChar {
match self {
PrefixKey::Tool => ChatChar {
codepoints: &["U+1F527"],
name: "wrench",
meaning: "a tool call started",
},
PrefixKey::Thinking => ChatChar {
codepoints: &["U+1F4AD"],
name: "thought balloon",
meaning: "the agent is thinking",
},
PrefixKey::Question => ChatChar {
codepoints: &["U+2753"],
name: "question mark",
meaning: "the agent is asking the user something",
},
PrefixKey::Warning => ChatChar {
codepoints: &["U+26A0", "U+FE0F"],
name: "warning sign",
meaning: "degraded state: shared workspace, backend gap, provider backoff",
},
PrefixKey::Delegated => ChatChar {
codepoints: &["U+1F4E4"],
name: "outbox tray",
meaning: "a question about one artefact was sent to a cheaper model",
},
PrefixKey::Connection => ChatChar {
codepoints: &["U+1F50C"],
name: "electric plug",
meaning: "connection or session lifecycle changed",
},
}
}
pub fn glyph(self) -> String {
glyph(&self.entry())
}
}
#[allow(
dead_code,
reason = "the tests walk the whole table, which is the point of enumerating it"
)]
pub fn all_chars() -> Vec<ChatChar> {
let mut all = Vec::new();
let reactions = [
ReactionKey::Accepted,
ReactionKey::Succeeded,
ReactionKey::Failed,
ReactionKey::Interrupted,
];
all.extend(reactions.map(ReactionKey::entry));
all.extend(ALL_PREFIXES.map(PrefixKey::entry));
all
}
fn parse_codepoint(codepoint: &str) -> u32 {
let Some(digits) = codepoint.strip_prefix("U+") else {
panic!("malformed codepoint {codepoint}; expected U+XXXX");
};
assert!(
!digits.is_empty() && digits.len() <= 6 && digits.bytes().all(|b| b.is_ascii_hexdigit()),
"malformed codepoint {codepoint}; expected U+XXXX"
);
u32::from_str_radix(digits, 16).expect("hex digits")
}
pub fn glyph(entry: &ChatChar) -> String {
entry
.codepoints
.iter()
.map(|codepoint| char::from_u32(parse_codepoint(codepoint)).expect("a valid codepoint"))
.collect()
}
pub fn reaction(key: ReactionKey) -> String {
key.glyph()
}
pub fn prefixed(key: PrefixKey, text: &str) -> String {
format!("{} {text}", key.glyph())
}
#[cfg(test)]
mod tests;
impl From<ReactionOutcome> for ReactionKey {
fn from(outcome: ReactionOutcome) -> Self {
match outcome {
ReactionOutcome::Accepted => ReactionKey::Accepted,
ReactionOutcome::Succeeded => ReactionKey::Succeeded,
ReactionOutcome::Failed => ReactionKey::Failed,
ReactionOutcome::Interrupted => ReactionKey::Interrupted,
}
}
}