use std::collections::HashMap;
use serde_json::Value;
use supercode::ChatMessage;
use supercode::FrontendEvent;
use supercode::Role;
use crate::semantic_value_summary;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TranscriptKind {
User,
Assistant,
Shell,
FileRead,
FileWrite,
Patch,
Mcp,
Approval,
Subagent,
Scheduled,
Reduction,
Reasoning,
Notice,
Usage,
Generic,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CellState {
Pending,
Complete,
Failed,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TranscriptCell {
pub kind: TranscriptKind,
pub state: CellState,
pub title: String,
pub body: String,
pub event_kind: Option<String>,
pub opaque_payload: Option<Value>,
}
impl TranscriptCell {
fn complete(kind: TranscriptKind, title: impl Into<String>, body: impl Into<String>) -> Self {
Self {
kind,
state: CellState::Complete,
title: title.into(),
body: body.into(),
event_kind: None,
opaque_payload: None,
}
}
fn pending(kind: TranscriptKind, title: impl Into<String>, body: impl Into<String>) -> Self {
Self {
kind,
state: CellState::Pending,
title: title.into(),
body: body.into(),
event_kind: None,
opaque_payload: None,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct TranscriptModel {
cells: Vec<TranscriptCell>,
tool_cells: HashMap<String, usize>,
streaming_cell: Option<usize>,
last_sequence: u64,
}
impl TranscriptModel {
pub fn from_history(history: &[ChatMessage], history_cursor: u64) -> Self {
let mut model = Self {
last_sequence: history_cursor,
..Self::default()
};
for message in history {
model.push_history_message(message);
}
model
}
pub fn cells(&self) -> &[TranscriptCell] {
&self.cells
}
pub fn last_sequence(&self) -> u64 {
self.last_sequence
}
pub fn resolve_frontend_request(&mut self, request_id: u64, resolution: &str) -> bool {
self.resolve_frontend_request_with_payload(request_id, resolution, None)
}
fn resolve_frontend_request_with_payload(
&mut self,
request_id: u64,
resolution: &str,
resolution_payload: Option<&Value>,
) -> bool {
let Some(cell) =
self.cells.iter_mut().rev().find(|cell| {
request_id_from_opaque(cell.opaque_payload.as_ref()) == Some(request_id)
})
else {
return false;
};
let mut changed = false;
if cell.state == CellState::Pending {
if !cell.body.is_empty() {
cell.body.push('\n');
}
cell.body.push_str("Resolution: ");
cell.body.push_str(resolution);
cell.state = CellState::Complete;
changed = true;
}
if let Some(resolution_payload) = resolution_payload {
let request_payload = cell.opaque_payload.take().unwrap_or(Value::Null);
cell.opaque_payload = Some(serde_json::json!({
"request_event": request_payload,
"resolution_event": resolution_payload,
}));
changed = true;
}
changed
}
pub fn apply_event(&mut self, event: &FrontendEvent) -> bool {
if event.sequence <= self.last_sequence {
return false;
}
self.last_sequence = event.sequence;
match event.kind.as_str() {
"user_message" => self.push_text_event(
TranscriptKind::User,
"You",
event.payload.get("text").and_then(Value::as_str),
),
"text_delta" => self.append_assistant_delta(
event
.payload
.get("text")
.and_then(Value::as_str)
.unwrap_or_default(),
),
"turn_completed" => self.finalize_assistant(),
"turn_started" | "turn_succeeded" => return false,
"turn_interrupted" => {
self.cells.push(TranscriptCell::complete(
TranscriptKind::Notice,
"Turn interrupted",
"The active SDK turn was interrupted.",
));
}
"turn_failed" => self.push_error(event),
"tool_call_started" => self.start_tool(event),
"tool_call_completed" => self.complete_tool(event),
"cache_warning" => self.push_text_event(
TranscriptKind::Notice,
"Cache warning",
event.payload.get("message").and_then(Value::as_str),
),
"usage" => self.push_usage(event),
"background_output" => self.push_background_output(event),
"request" => self.push_request(event),
"request_resolved" => self.apply_request_resolution(event),
"scheduled_prompt_started"
| "scheduled_prompt_deferred"
| "scheduled_prompt_completed"
| "scheduler_error" => self.push_scheduled(event),
"stream_error" | "transport_error" | "watch_error" | "runtime_disconnected" => {
self.push_error(event)
}
"reasoning" | "thinking" => self.push_reasoning(event),
_ => self.push_generic(event),
}
true
}
fn push_history_message(&mut self, message: &ChatMessage) {
let text = message_text(message);
match message.role {
Role::System => self.cells.push(TranscriptCell::complete(
TranscriptKind::Notice,
"System",
text,
)),
Role::User => {
self.cells
.push(TranscriptCell::complete(TranscriptKind::User, "You", text))
}
Role::Assistant => {
if !text.is_empty() {
self.cells.push(TranscriptCell::complete(
TranscriptKind::Assistant,
"Assistant",
text,
));
}
for call in message.tool_calls() {
let kind = classify_tool(&call.function.name, &call.function.arguments, "");
let index = self.cells.len();
self.cells.push(TranscriptCell::pending(
kind,
tool_title(kind, &call.function.name),
format_arguments(&call.function.arguments),
));
self.tool_cells.insert(call.id.clone(), index);
}
}
Role::Tool => {
let id = message.tool_call_id.as_deref().unwrap_or_default();
if let Some(index) = self.tool_cells.remove(id) {
let cell = &mut self.cells[index];
if is_reduction(&cell.title, &cell.body, &text) {
cell.kind = TranscriptKind::Reduction;
cell.title = "Reduced context".into();
}
cell.body = text;
cell.state = CellState::Complete;
} else {
let name = message.name.as_deref().unwrap_or("tool");
let kind = classify_tool(name, "", &text);
self.cells
.push(TranscriptCell::complete(kind, tool_title(kind, name), text));
}
}
}
}
fn push_text_event(&mut self, kind: TranscriptKind, title: &str, text: Option<&str>) {
self.cells.push(TranscriptCell::complete(
kind,
title,
text.unwrap_or_default(),
));
}
fn append_assistant_delta(&mut self, delta: &str) {
let index = match self.streaming_cell {
Some(index) => index,
None => {
let index = self.cells.len();
self.cells.push(TranscriptCell::pending(
TranscriptKind::Assistant,
"Assistant",
"",
));
self.streaming_cell = Some(index);
index
}
};
self.cells[index].body.push_str(delta);
}
fn finalize_assistant(&mut self) {
if let Some(index) = self.streaming_cell.take() {
self.cells[index].state = CellState::Complete;
}
}
fn start_tool(&mut self, event: &FrontendEvent) {
let id = string_field(&event.payload, "id");
let name = string_field(&event.payload, "name");
let arguments = string_field(&event.payload, "arguments");
let kind = classify_tool(name, arguments, "");
let index = self.cells.len();
self.cells.push(TranscriptCell::pending(
kind,
tool_title(kind, name),
format_arguments(arguments),
));
if !id.is_empty() {
self.tool_cells.insert(id.to_string(), index);
}
}
fn complete_tool(&mut self, event: &FrontendEvent) {
let id = string_field(&event.payload, "id");
let name = string_field(&event.payload, "name");
let output = string_field(&event.payload, "output");
let failed = event
.payload
.get("is_error")
.and_then(Value::as_bool)
.unwrap_or(false);
if let Some(index) = self.tool_cells.remove(id) {
let cell = &mut self.cells[index];
if is_reduction(name, &cell.body, output) {
cell.kind = TranscriptKind::Reduction;
cell.title = "Reduced context".into();
}
cell.body = output.to_string();
cell.state = if failed {
CellState::Failed
} else {
CellState::Complete
};
return;
}
let kind = classify_tool(name, "", output);
let mut cell = TranscriptCell::complete(kind, tool_title(kind, name), output);
if failed {
cell.state = CellState::Failed;
}
self.cells.push(cell);
}
fn push_usage(&mut self, event: &FrontendEvent) {
let prompt = event.payload.get("prompt_tokens").and_then(Value::as_u64);
let completion = event
.payload
.get("completion_tokens")
.and_then(Value::as_u64);
let total = event.payload.get("total_tokens").and_then(Value::as_u64);
let body = format!(
"prompt {} · completion {} · total {}",
token_value(prompt),
token_value(completion),
token_value(total)
);
self.cells.push(TranscriptCell::complete(
TranscriptKind::Usage,
"Usage",
body,
));
}
fn push_background_output(&mut self, event: &FrontendEvent) {
let job = string_field(&event.payload, "job_id");
let mut body = string_field(&event.payload, "chunk").to_string();
if event
.payload
.get("truncated")
.and_then(Value::as_bool)
.unwrap_or(false)
{
body.push_str("\n[output truncated]");
}
self.cells.push(TranscriptCell::complete(
TranscriptKind::Scheduled,
format!("Background job {job}"),
body,
));
}
fn push_request(&mut self, event: &FrontendEvent) {
let request = event.payload.get("request").unwrap_or(&event.payload);
let request_kind = request
.get("kind")
.and_then(Value::as_str)
.unwrap_or("request");
let kind = match request_kind {
"approval" => TranscriptKind::Approval,
"elicitation" => TranscriptKind::Mcp,
_ => TranscriptKind::Generic,
};
let payload = request.get("payload").unwrap_or(request);
let label = match kind {
TranscriptKind::Approval => "Approval request",
TranscriptKind::Mcp => "MCP input request",
_ => "Runtime input request",
};
self.cells.push(TranscriptCell {
kind,
state: CellState::Pending,
title: match kind {
TranscriptKind::Approval => "Approval requested".into(),
TranscriptKind::Mcp => "MCP input requested".into(),
_ => "Runtime input requested".into(),
},
body: semantic_value_summary(payload, label, 512),
event_kind: Some(event.kind.clone()),
opaque_payload: Some(event.payload.clone()),
});
}
fn apply_request_resolution(&mut self, event: &FrontendEvent) {
let Some(request_id) = event.payload.get("request_id").and_then(Value::as_u64) else {
return;
};
let resolution = event
.payload
.get("response")
.map(frontend_response_summary)
.unwrap_or_else(|| "resolved".into());
self.resolve_frontend_request_with_payload(request_id, &resolution, Some(&event.payload));
}
fn push_scheduled(&mut self, event: &FrontendEvent) {
let failed = event.kind == "scheduler_error";
self.cells.push(TranscriptCell {
kind: TranscriptKind::Scheduled,
state: if failed {
CellState::Failed
} else {
CellState::Complete
},
title: event.kind.replace('_', " "),
body: event_summary(&event.payload),
event_kind: Some(event.kind.clone()),
opaque_payload: Some(event.payload.clone()),
});
}
fn push_error(&mut self, event: &FrontendEvent) {
self.cells.push(TranscriptCell {
kind: TranscriptKind::Notice,
state: CellState::Failed,
title: format!("Error: {}", event.kind.replace('_', " ")),
body: event_summary(&event.payload),
event_kind: Some(event.kind.clone()),
opaque_payload: Some(event.payload.clone()),
});
}
fn push_reasoning(&mut self, event: &FrontendEvent) {
let body = event
.payload
.get("text")
.or_else(|| event.payload.get("summary"))
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| compact_json(&event.payload));
self.cells.push(TranscriptCell::complete(
TranscriptKind::Reasoning,
"Reasoning",
body,
));
}
fn push_generic(&mut self, event: &FrontendEvent) {
self.cells.push(TranscriptCell {
kind: TranscriptKind::Generic,
state: CellState::Complete,
title: format!("Event: {}", event.kind),
body: "This runtime event is not displayable by this frontend version.".into(),
event_kind: Some(event.kind.clone()),
opaque_payload: Some(event.payload.clone()),
});
}
}
fn message_text(message: &ChatMessage) -> String {
if let Some(content) = &message.content {
return content.clone();
}
message
.content_parts
.as_deref()
.unwrap_or_default()
.iter()
.filter_map(|part| match part.get("type").and_then(Value::as_str) {
Some("text") | Some("input_text") | Some("output_text") => {
part.get("text").and_then(Value::as_str).map(str::to_string)
}
Some("image") | Some("image_url") | Some("input_image") => Some("[image]".into()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
fn classify_tool(name: &str, arguments: &str, output: &str) -> TranscriptKind {
let lower = name.to_ascii_lowercase();
if is_reduction(&lower, arguments, output) {
TranscriptKind::Reduction
} else if lower.contains("subagent")
|| matches!(
lower.as_str(),
"task" | "spawn_agent" | "send_message" | "followup_task"
)
{
TranscriptKind::Subagent
} else if lower.contains("cron")
|| lower.contains("schedule")
|| lower.starts_with("background_")
{
TranscriptKind::Scheduled
} else if lower.starts_with("mcp") || lower.contains("__mcp__") {
TranscriptKind::Mcp
} else if lower == "apply_patch" || lower.contains("patch") {
TranscriptKind::Patch
} else if lower.contains("write") || lower.contains("edit") || lower.contains("notebook") {
TranscriptKind::FileWrite
} else if lower.contains("read")
|| lower.contains("glob")
|| lower.contains("search")
|| lower.contains("list_dir")
|| lower == "view_image"
{
TranscriptKind::FileRead
} else {
TranscriptKind::Shell
}
}
fn tool_title(kind: TranscriptKind, name: &str) -> String {
let action = match kind {
TranscriptKind::Shell => "Command",
TranscriptKind::FileRead => "Read",
TranscriptKind::FileWrite => "Write",
TranscriptKind::Patch => "Patch",
TranscriptKind::Mcp => "MCP",
TranscriptKind::Subagent => "Subagent",
TranscriptKind::Scheduled => "Scheduled",
TranscriptKind::Reduction => "Reduced context",
_ => "Tool",
};
if name.is_empty() || kind == TranscriptKind::Reduction {
action.into()
} else {
format!("{action}: {name}")
}
}
fn is_reduction(name: &str, arguments: &str, output: &str) -> bool {
let name = name.to_ascii_lowercase();
name.contains("reduction")
|| name.contains("rehydrate")
|| arguments.contains("[reduction:")
|| output.contains("[reduction:")
|| output.contains("<reduction ")
}
fn format_arguments(arguments: &str) -> String {
let Ok(value) = serde_json::from_str::<Value>(arguments) else {
return arguments.to_string();
};
compact_json(&value)
}
fn compact_json(value: &Value) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "<unavailable>".into())
}
fn event_summary(payload: &Value) -> String {
semantic_value_summary(payload, "Runtime event", 512)
}
fn request_id_from_opaque(payload: Option<&Value>) -> Option<u64> {
let payload = payload?;
let request_event = payload.get("request_event").unwrap_or(payload);
request_event
.get("request")
.unwrap_or(request_event)
.get("id")
.and_then(Value::as_u64)
}
fn frontend_response_summary(response: &Value) -> String {
match response.get("kind").and_then(Value::as_str) {
Some("approval") => match response.get("decision").and_then(Value::as_str) {
Some("deny") => "denied".into(),
Some("allow") => "allowed once".into(),
Some("allow_for_session") => "allowed for session".into(),
Some(decision) => format!(
"approval: {}",
semantic_value_summary(&Value::String(decision.into()), "decision", 128)
),
None => "approval resolved".into(),
},
Some("elicitation" | "other") => {
let action = response
.get("action")
.and_then(Value::as_str)
.unwrap_or("resolved");
if let Some(content) = response.get("content").filter(|content| !content.is_null()) {
format!(
"{} · {}",
semantic_value_summary(&Value::String(action.into()), "action", 64),
semantic_value_summary(content, "content", 192)
)
} else {
semantic_value_summary(&Value::String(action.into()), "action", 128)
}
}
_ => semantic_value_summary(response, "Response", 256),
}
}
fn string_field<'a>(value: &'a Value, name: &str) -> &'a str {
value.get(name).and_then(Value::as_str).unwrap_or_default()
}
fn token_value(value: Option<u64>) -> String {
value.map_or_else(|| "?".into(), |value| value.to_string())
}
#[cfg(test)]
mod tests {
use serde_json::json;
use supercode::FunctionCall;
use supercode::ToolCall;
use super::*;
fn event(sequence: u64, kind: &str, payload: Value) -> FrontendEvent {
FrontendEvent {
sequence,
kind: kind.into(),
payload,
}
}
#[test]
fn streaming_deltas_update_and_finalize_one_cell() {
let mut model = TranscriptModel::default();
assert!(model.apply_event(&event(
1,
"text_delta",
json!({"type":"text_delta", "text":"hello "})
)));
assert!(model.apply_event(&event(
2,
"text_delta",
json!({"type":"text_delta", "text":"world"})
)));
assert_eq!(model.cells.len(), 1);
assert_eq!(model.cells[0].body, "hello world");
assert_eq!(model.cells[0].state, CellState::Pending);
model.apply_event(&event(
3,
"turn_completed",
json!({"type":"turn_completed"}),
));
assert_eq!(model.cells.len(), 1);
assert_eq!(model.cells[0].state, CellState::Complete);
}
#[test]
fn canonical_cursor_and_duplicate_sequences_never_repeat_content() {
let history = vec![
ChatMessage::user("prompt"),
ChatMessage::assistant("answer"),
];
let mut model = TranscriptModel::from_history(&history, 8);
assert!(!model.apply_event(&event(
8,
"user_message",
json!({"type":"user_message", "text":"prompt"})
)));
assert!(model.apply_event(&event(
9,
"user_message",
json!({"type":"user_message", "text":"next"})
)));
assert!(!model.apply_event(&event(
9,
"user_message",
json!({"type":"user_message", "text":"next"})
)));
assert_eq!(
model
.cells
.iter()
.filter(|cell| cell.body == "prompt")
.count(),
1
);
assert_eq!(
model
.cells
.iter()
.filter(|cell| cell.body == "answer")
.count(),
1
);
assert_eq!(
model
.cells
.iter()
.filter(|cell| cell.body == "next")
.count(),
1
);
}
#[test]
fn history_pairs_tool_calls_and_results_into_one_cell() {
let assistant = ChatMessage {
role: Role::Assistant,
content: None,
content_parts: None,
tool_calls: Some(vec![ToolCall {
id: "call-1".into(),
kind: "function".into(),
function: FunctionCall {
name: "read_file".into(),
arguments: r#"{"path":"src/lib.rs"}"#.into(),
},
}]),
tool_call_id: None,
name: None,
metadata: Default::default(),
};
let model = TranscriptModel::from_history(
&[
assistant,
ChatMessage::tool_result("call-1", "read_file", "source"),
],
4,
);
assert_eq!(model.cells.len(), 1);
assert_eq!(model.cells[0].kind, TranscriptKind::FileRead);
assert_eq!(model.cells[0].state, CellState::Complete);
assert_eq!(model.cells[0].body, "source");
}
#[test]
fn semantic_tool_families_render_without_provider_types() {
let cases = [
("exec_command", TranscriptKind::Shell),
("read_file", TranscriptKind::FileRead),
("write_file", TranscriptKind::FileWrite),
("apply_patch", TranscriptKind::Patch),
("mcp__db__query", TranscriptKind::Mcp),
("spawn_agent", TranscriptKind::Subagent),
("cron_create", TranscriptKind::Scheduled),
("expand_reduction", TranscriptKind::Reduction),
];
let mut model = TranscriptModel::default();
for (offset, (name, expected)) in cases.into_iter().enumerate() {
let sequence = offset as u64 * 2 + 1;
model.apply_event(&event(
sequence,
"tool_call_started",
json!({"type":"tool_call_started", "id":format!("c{offset}"), "name":name, "arguments":"{}"}),
));
assert_eq!(model.cells.last().unwrap().kind, expected);
model.apply_event(&event(
sequence + 1,
"tool_call_completed",
json!({"type":"tool_call_completed", "id":format!("c{offset}"), "name":name, "output":"ok", "is_error":false}),
));
assert_eq!(model.cells.last().unwrap().state, CellState::Complete);
}
}
#[test]
fn failures_approvals_schedules_and_reduction_stubs_are_semantic() {
let mut model = TranscriptModel::default();
model.apply_event(&event(
1,
"tool_call_completed",
json!({"type":"tool_call_completed", "id":"bad", "name":"exec_command", "output":"denied", "is_error":true}),
));
model.apply_event(&event(
2,
"request",
json!({"type":"request", "request":{"kind":"approval", "payload":{"tool":"bash"}}}),
));
model.apply_event(&event(
3,
"scheduled_prompt_started",
json!({"type":"scheduled_prompt_started", "name":"nightly"}),
));
model.apply_event(&event(
4,
"tool_call_completed",
json!({"type":"tool_call_completed", "id":"r", "name":"read_file", "output":"[reduction:r1 kind=tool_output]", "is_error":false}),
));
model.apply_event(&event(
5,
"stream_error",
json!({"type":"stream_error", "message":"network lost"}),
));
assert_eq!(model.cells[0].state, CellState::Failed);
assert_eq!(model.cells[1].kind, TranscriptKind::Approval);
assert_eq!(model.cells[2].kind, TranscriptKind::Scheduled);
assert_eq!(model.cells[3].kind, TranscriptKind::Reduction);
assert_eq!(model.cells[4].kind, TranscriptKind::Notice);
assert_eq!(model.cells[4].state, CellState::Failed);
assert_eq!(model.cells[4].body, "network lost");
}
#[test]
fn typed_frontend_resolution_completes_original_request_cell() {
let mut model = TranscriptModel::default();
model.apply_event(&event(
1,
"request",
json!({"type":"request", "request":{"id":42, "kind":"approval", "payload":{"tool":"bash"}}}),
));
assert_eq!(model.cells()[0].state, CellState::Pending);
assert!(model.resolve_frontend_request(42, "allowed once"));
assert_eq!(model.cells()[0].state, CellState::Complete);
assert!(model.cells()[0].body.contains("Resolution: allowed once"));
assert!(model.cells()[0].opaque_payload.is_some());
assert!(!model.resolve_frontend_request(42, "denied"));
}
#[test]
fn unknown_event_is_human_safe_and_retains_full_opaque_payload() {
let payload =
json!({"type":"future_event", "large":"x".repeat(2000), "nested":{"kept":true}});
let mut model = TranscriptModel::default();
model.apply_event(&event(1, "future_event", payload.clone()));
let cell = &model.cells[0];
assert_eq!(cell.kind, TranscriptKind::Generic);
assert_eq!(cell.title, "Event: future_event");
assert!(!cell.body.contains("nested"));
assert!(!cell.body.starts_with('{'));
assert_eq!(cell.opaque_payload.as_ref(), Some(&payload));
}
#[test]
fn adversarial_request_is_bounded_and_exact_only_in_opaque_storage() {
let payload = json!({
"type":"request",
"request": {
"id": 77,
"kind":"approval",
"payload": {
"nested": {"ansi": format!("\u{1b}]0;owned\u{7}{}", "x".repeat(10_000))}
}
}
});
let mut model = TranscriptModel::default();
model.apply_event(&event(1, "request", payload.clone()));
let cell = &model.cells()[0];
assert!(cell.body.chars().count() <= 512, "{}", cell.body.len());
assert!(!cell.body.contains("x".repeat(1_000).as_str()));
assert!(!cell.body.contains('\u{1b}'));
assert_eq!(cell.opaque_payload.as_ref(), Some(&payload));
}
#[test]
fn canonical_request_resolution_completes_pending_cell_for_all_observers() {
let mut model = TranscriptModel::default();
let request_event =
json!({"request":{"id":42,"kind":"approval","payload":{"tool":"bash"}}});
let resolution_event = json!({
"request_id":42,
"response":{"kind":"approval","request_id":42,"decision":"allow"}
});
model.apply_event(&event(1, "request", request_event.clone()));
model.apply_event(&event(2, "request_resolved", resolution_event.clone()));
assert_eq!(model.cells()[0].state, CellState::Complete);
assert!(model.cells()[0].body.contains("Resolution: allowed once"));
let retained = json!({
"request_event": request_event,
"resolution_event": resolution_event,
});
assert_eq!(model.cells()[0].opaque_payload.as_ref(), Some(&retained));
}
#[test]
fn canonical_resolution_enriches_an_optimistically_completed_request() {
let mut model = TranscriptModel::default();
model.apply_event(&event(
1,
"request",
json!({"request":{"id":7,"kind":"approval","payload":{"tool":"bash"}}}),
));
assert!(model.resolve_frontend_request(7, "allowed once"));
model.apply_event(&event(
2,
"request_resolved",
json!({"request_id":7,"response":{"kind":"approval","request_id":7,"decision":"allow"}}),
));
let cell = &model.cells()[0];
assert_eq!(cell.body.matches("Resolution:").count(), 1);
assert_eq!(
cell.opaque_payload.as_ref().unwrap()["resolution_event"]["response"]["decision"],
"allow"
);
}
}