use super::*;
use crate::output::ActivityId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ToolOutcome {
Success,
Failure,
Cancelled,
Interrupted,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ChangeState {
NotApplicable,
None,
Applied,
Partial,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CardStatus {
Running,
Success,
Failed,
Canceled,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ToolCardRole {
Bash,
CodeMode,
MagiControl,
Read,
Grep,
AstGrep,
Find,
ListFiles,
Web,
Tool,
Write,
HashEdit,
SubagentBatch,
ViewImage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TextMeaning {
Plain,
Metadata,
Resource,
Pattern,
Count,
Separator,
OutcomeMark,
Added,
Removed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TextSpan {
pub(crate) text: String,
pub(crate) meaning: TextMeaning,
}
impl TextSpan {
pub(super) fn new(text: impl Into<String>, meaning: TextMeaning) -> Self {
Self {
text: text.into(),
meaning,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TextLine {
pub(crate) spans: Vec<TextSpan>,
}
impl TextLine {
pub(super) fn from_span(text: impl Into<String>, meaning: TextMeaning) -> Self {
Self {
spans: vec![TextSpan::new(text, meaning)],
}
}
pub(super) fn plain(text: impl Into<String>) -> Self {
Self::from_span(text, TextMeaning::Plain)
}
pub(crate) fn plain_text(&self) -> String {
self.spans.iter().map(|span| span.text.as_str()).collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DiffApplication {
Applied,
Partial,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResourceAvailability {
Available,
Unavailable,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResourceRow {
pub(crate) line: TextLine,
pub(crate) location: String,
pub(crate) availability: ResourceAvailability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResultOutcome {
Success,
Failure,
Partial,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResultRow {
pub(crate) label: String,
pub(crate) value: Option<String>,
pub(crate) outcome: ResultOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ToolBlock {
Text {
lines: Vec<TextLine>,
},
Command {
command: Option<String>,
working_directory: Option<String>,
exit_code: Option<i64>,
},
Resources {
rows: Vec<ResourceRow>,
},
Diff {
application: DiffApplication,
lines: Vec<TextLine>,
},
Progress {
label: String,
completed: Option<u64>,
total: Option<u64>,
},
Results {
rows: Vec<ResultRow>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OmissionSubject {
Title,
Metadata,
Blocks,
Rows,
Text,
DiffLines,
Children,
Progress,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum OmissionReason {
Redacted,
MissingSource,
NotRecorded,
UnsupportedSource,
SizeLimit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DisplayOmission {
pub(crate) subject: OmissionSubject,
pub(crate) reason: OmissionReason,
pub(crate) omitted_count: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ToolCard {
pub(crate) role: ToolCardRole,
pub(crate) status: CardStatus,
pub(crate) outcome: Option<ToolOutcome>,
pub(crate) changes: ChangeState,
pub(crate) public_protection: PublicProtection,
pub(crate) title: String,
pub(crate) metadata: Vec<String>,
pub(crate) body: String,
pub(crate) blocks: Vec<ToolBlock>,
pub(crate) activity_rows: Vec<ToolActivityRow>,
pub(crate) omissions: Vec<DisplayOmission>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ToolActivityRow {
pub(crate) label: String,
pub(crate) status: CardStatus,
pub(crate) activity_id: ActivityId,
}
pub(crate) struct ChildToolFacts<'a> {
pub(crate) id: &'a ActivityId,
pub(crate) status: ActivityStatus,
pub(crate) tool_name: Option<&'a str>,
pub(crate) detail: Option<&'a ToolActivityDetail>,
pub(crate) safe_target: Option<String>,
}
pub(super) struct CardDraft {
pub(super) role: ToolCardRole,
pub(super) status: CardStatus,
pub(super) title: String,
pub(super) metadata: Vec<String>,
pub(super) body: String,
pub(super) body_lines: Vec<TextLine>,
pub(super) activity_rows: Vec<ToolActivityRow>,
}
pub(super) fn blocks_from_draft(
draft: &CardDraft,
input: &ToolCardInput<'_>,
changes: ChangeState,
) -> Vec<ToolBlock> {
let mut blocks = Vec::new();
if draft.role == ToolCardRole::Bash && !input.bash_mode_response {
if let Some(intent) = input
.detail
.and_then(|detail| detail.params["intent"].as_str())
.map(str::trim)
.filter(|intent| !intent.is_empty())
{
blocks.push(ToolBlock::Text {
lines: vec![TextLine::plain(sanitized_single_line(intent))],
});
}
blocks.push(ToolBlock::Command {
command: input.bash_command.map(sanitize_display_text),
working_directory: None,
exit_code: input
.detail
.and_then(|detail| detail.metadata["exit_code"].as_i64()),
});
let rows = super::bash_notices::bash_execution_notices(input.detail);
if !rows.is_empty() {
blocks.push(ToolBlock::Results {
rows: rows
.into_iter()
.map(|notice| ResultRow {
label: notice.label.to_owned(),
value: notice.value,
outcome: ResultOutcome::Unknown,
})
.collect(),
});
}
}
let lines = if draft.body_lines.is_empty() && draft.role != ToolCardRole::Bash {
draft.body.lines().map(TextLine::plain).collect()
} else {
draft.body_lines.clone()
};
for mut line in lines {
for span in &mut line.spans {
span.text = sanitize_display_text(&span.text);
}
let resource = line
.spans
.iter()
.find(|span| span.meaning == TextMeaning::Resource)
.map(|span| span.text.clone());
let diff = line
.spans
.iter()
.any(|span| matches!(span.meaning, TextMeaning::Added | TextMeaning::Removed));
if diff {
let application = match changes {
ChangeState::Applied => DiffApplication::Applied,
ChangeState::Partial => DiffApplication::Partial,
_ => DiffApplication::Unknown,
};
if let Some(ToolBlock::Diff { lines, .. }) = blocks.last_mut() {
lines.push(line);
} else {
blocks.push(ToolBlock::Diff {
application,
lines: vec![line],
});
}
} else if let Some(location) = resource {
let row = ResourceRow {
line,
location,
availability: ResourceAvailability::Unknown,
};
if let Some(ToolBlock::Resources { rows }) = blocks.last_mut() {
rows.push(row);
} else {
blocks.push(ToolBlock::Resources { rows: vec![row] });
}
} else if let Some(ToolBlock::Text { lines }) = blocks.last_mut() {
lines.push(line);
} else {
blocks.push(ToolBlock::Text { lines: vec![line] });
}
}
blocks
}