mod capabilities;
mod composer;
mod paste_burst;
mod transcript;
pub use capabilities::ComposerCapabilities;
pub use composer::{
ComposerAction, ComposerInputEvent, ComposerKey, ComposerKeyCode, ComposerModel,
ComposerOverlay, ComposerOverlayKind, ComposerTurnState,
};
pub use paste_burst::{CharDecision, FlushResult, PasteBurst, RetroGrab};
pub use transcript::{CellState, TranscriptCell, TranscriptKind, TranscriptModel};
use serde_json::Value;
pub fn semantic_value_summary(value: &Value, label: &str, limit: usize) -> String {
for field in [
"summary", "prompt", "message", "question", "tool", "command", "reason", "detail", "error",
"name",
] {
if let Some(text) = value.get(field).and_then(Value::as_str) {
return bounded_text(text, limit);
}
}
let summary = match value {
Value::Object(fields) if fields.is_empty() => format!("{label} (no details)"),
Value::Object(fields) => {
let mut names = fields
.keys()
.take(6)
.map(|name| bounded_metadata(name, 32))
.collect::<Vec<_>>();
if fields.len() > names.len() {
names.push("…".into());
}
format!("{label} (fields: {})", names.join(", "))
}
Value::Array(items) => format!("{label} ({} items)", items.len()),
Value::String(text) => bounded_text(text, limit),
Value::Number(number) => format!("{label}: {number}"),
Value::Bool(value) => format!("{label}: {value}"),
Value::Null => format!("{label} (no details)"),
};
bounded_text(&summary, limit)
}
fn bounded_metadata(input: &str, limit: usize) -> String {
let flattened = strip_controls(input)
.chars()
.map(|character| match character {
'\n' | '\t' => ' ',
character => character,
})
.collect::<String>();
bounded_text(&flattened, limit)
}
fn bounded_text(input: &str, limit: usize) -> String {
let sanitized = strip_controls(input);
if sanitized.chars().count() <= limit {
return sanitized;
}
let mut preview = sanitized
.chars()
.take(limit.saturating_sub(1))
.collect::<String>();
preview.push('…');
preview
}
fn strip_controls(input: &str) -> String {
#[derive(Clone, Copy)]
enum State {
Text,
Escape,
Csi,
Osc,
OscEscape,
}
let mut state = State::Text;
let mut output = String::with_capacity(input.len());
for character in input.chars() {
state = match state {
State::Text if character == '\u{1b}' => State::Escape,
State::Text => {
if character == '\n' || character == '\t' || !character.is_control() {
output.push(character);
}
State::Text
}
State::Escape if character == '[' => State::Csi,
State::Escape if character == ']' => State::Osc,
State::Escape => State::Text,
State::Csi if ('@'..='~').contains(&character) => State::Text,
State::Csi => State::Csi,
State::Osc if character == '\u{7}' => State::Text,
State::Osc if character == '\u{1b}' => State::OscEscape,
State::Osc => State::Osc,
State::OscEscape if character == '\\' => State::Text,
State::OscEscape if character == '\u{1b}' => State::OscEscape,
State::OscEscape => State::Osc,
};
}
output
}